Skip to content

Commit

Permalink
Merge pull request #5278 from Textualize/demo-tweaks
Browse files Browse the repository at this point in the history
Add position:absolute and game to demo
  • Loading branch information
willmcgugan authored Nov 24, 2024
2 parents d9acae2 + 7279db8 commit de9897b
Show file tree
Hide file tree
Showing 30 changed files with 1,194 additions and 36 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).


## Unreleased
## [0.87.0] - 2024-11-24

### Added

- Added Styles.has_any_rules https://github.com/Textualize/textual/pull/5264
- Added `position` CSS rule. https://github.com/Textualize/textual/pull/5278
- Added `Widget.set_scroll` https://github.com/Textualize/textual/pull/5278
- Added `Select.selection` https://github.com/Textualize/textual/pull/5278

### Fixed

Expand Down Expand Up @@ -2573,6 +2576,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[0.87.0]: https://github.com/Textualize/textual/compare/v0.86.4...v0.87.0
[0.86.3]: https://github.com/Textualize/textual/compare/v0.86.2...v0.86.3
[0.86.2]: https://github.com/Textualize/textual/compare/v0.86.1...v0.86.2
[0.86.1]: https://github.com/Textualize/textual/compare/v0.86.0...v0.86.1
Expand Down
31 changes: 31 additions & 0 deletions docs/css_types/position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# <position>

The `<position>` CSS type defines how the the `offset` rule is applied..


## Syntax

A [`<position>`](./position.md) may be any of the following values:

| Value | Alignment type |
| ---------- | ------------------------------------------------------------ |
| `relative` | Offset is applied to widgets default position. |
| `absolute` | Offset is applied to the origin (top left) of its container. |

## Examples

### CSS

```css
Label {
position: absolute;
offset: 10 5;
}
```

### Python

```py
widget.styles.position = "absolute"
widget.styles.offset = (10, 5)
```
15 changes: 15 additions & 0 deletions docs/examples/styles/position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from textual.app import App, ComposeResult
from textual.widgets import Label


class PositionApp(App):
CSS_PATH = "position.tcss"

def compose(self) -> ComposeResult:
yield Label("Absolute", id="label1")
yield Label("Relative", id="label2")


if __name__ == "__main__":
app = PositionApp()
app.run()
19 changes: 19 additions & 0 deletions docs/examples/styles/position.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Screen {
align: center middle;
}

Label {
padding: 1;
background: $panel;
border: thick $border;
}

Label#label1 {
position: absolute;
offset: 2 1;
}

Label#label2 {
position: relative;
offset: 2 1;
}
61 changes: 61 additions & 0 deletions docs/styles/position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

# Position

The `position` style modifies what [`offset`](./offset.md) is applied to.
The default for `position` is `"relative"`, which means the offset is applied to the normal position of the widget.
In other words, if `offset` is (1, 1), then the widget will be moved 1 cell and 1 line down from its usual position.

The alternative value of `position` is `"absolute"`.
With absolute positioning, the offset is relative to the origin (i.e. the top left of the container).
So a widget with offset (1, 1) and absolute positioning will be 1 cell and 1 line down from the top left corner.

!!! note

Absolute positioning takes precedence over the parent's alignment rule.

## Syntax

--8<-- "docs/snippets/syntax_block_start.md"
position: <a href="../../css_types/position">&lt;position&gt;</a>;
--8<-- "docs/snippets/syntax_block_end.md"


## Examples


Two labels, the first is absolute positioned and is displayed relative to the top left of the screen.
The second label is relative and is displayed offset from the center.

=== "Output"

```{.textual path="docs/examples/styles/position.py"}
```

=== "position.py"

```py
--8<-- "docs/examples/styles/position.py"
```

=== "position.tcss"

```css
--8<-- "docs/examples/styles/position.tcss"
```




## CSS

```css
position: relative;
position: absolute;
```

## Python

```py
widget.styles.position = "relative"
widget.styles.position = "absolute"
```
2 changes: 2 additions & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ nav:
- "css_types/name.md"
- "css_types/number.md"
- "css_types/overflow.md"
- "css_types/position.md"
- "css_types/percentage.md"
- "css_types/scalar.md"
- "css_types/text_align.md"
Expand Down Expand Up @@ -122,6 +123,7 @@ nav:
- "styles/outline.md"
- "styles/overflow.md"
- "styles/padding.md"
- "styles/position.md"
- Scrollbar colors:
- "styles/scrollbar_colors/index.md"
- "styles/scrollbar_colors/scrollbar_background.md"
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.86.3"
version = "0.87.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand All @@ -22,6 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
include = [
Expand Down
5 changes: 4 additions & 1 deletion src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ def arrange(
layout_placements, placement_offset
)

WidgetPlacement.apply_absolute(layout_placements)

placements.extend(layout_placements)

return DockArrangeResult(placements, set(display_widgets), scroll_spacing)
Expand Down Expand Up @@ -186,6 +188,7 @@ def _arrange_dock_widgets(
dock_widget,
top_z,
True,
False,
)
)

Expand Down Expand Up @@ -238,7 +241,7 @@ def _arrange_split_widgets(

append_placement(
_WidgetPlacement(
split_region, null_offset, null_spacing, split_widget, 1, True
split_region, null_offset, null_spacing, split_widget, 1, True, False
)
)

Expand Down
1 change: 1 addition & 0 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ def add_widget(
z,
fixed,
overlay,
absolute,
) in reversed(placements):
layer_index = get_layer_index(sub_widget.layer, 0)
# Combine regions with children to calculate the "virtual size"
Expand Down
18 changes: 18 additions & 0 deletions src/textual/css/_help_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
VALID_BORDER,
VALID_KEYLINE,
VALID_LAYOUT,
VALID_POSITION,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
)
Expand Down Expand Up @@ -770,6 +771,23 @@ def offset_single_axis_help_text(property_name: str) -> HelpText:
)


def position_help_text(property_name: str) -> HelpText:
"""Help text to show when the user supplies the wrong value for position.
Args:
property_name: The name of the property.
Returns:
Renderable for displaying the help text for this property.
"""
return HelpText(
summary=f"Invalid value for [i]{property_name}[/]",
bullets=[
Bullet(f"Valid values are {friendly_list(VALID_POSITION)}"),
],
)


def style_flags_property_help_text(
property_name: str, value: str, context: StylingContext
) -> HelpText:
Expand Down
13 changes: 13 additions & 0 deletions src/textual/css/_styles_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
layout_property_help_text,
offset_property_help_text,
offset_single_axis_help_text,
position_help_text,
property_invalid_value_help_text,
scalar_help_text,
scrollbar_size_property_help_text,
Expand All @@ -47,6 +48,7 @@
VALID_KEYLINE,
VALID_OVERFLOW,
VALID_OVERLAY,
VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
Expand Down Expand Up @@ -622,6 +624,17 @@ def process_offset_y(self, name: str, tokens: list[Token]) -> None:
x = self.styles.offset.x
self.styles._rules["offset"] = ScalarOffset(x, y)

def process_position(self, name: str, tokens: list[Token]):
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], offset_single_axis_help_text(name))
else:
token = tokens[0]
if token.value not in VALID_POSITION:
self.error(name, tokens[0], position_help_text(name))
self.styles._rules["position"] = token.value

def process_layout(self, name: str, tokens: list[Token]) -> None:
from textual.layouts.factory import MissingLayout, get_layout

Expand Down
1 change: 1 addition & 0 deletions src/textual/css/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"}
VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"}
VALID_POSITION: Final = {"relative", "absolute"}
VALID_TEXT_ALIGN: Final = {
"start",
"end",
Expand Down
8 changes: 8 additions & 0 deletions src/textual/css/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
VALID_DISPLAY,
VALID_OVERFLOW,
VALID_OVERLAY,
VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_TEXT_ALIGN,
VALID_VISIBILITY,
Expand Down Expand Up @@ -99,6 +100,7 @@ class RulesMap(TypedDict, total=False):
padding: Spacing
margin: Spacing
offset: ScalarOffset
position: str

border_top: tuple[str, Color]
border_right: tuple[str, Color]
Expand Down Expand Up @@ -219,6 +221,7 @@ class StylesBase:
"background",
"background_tint",
"opacity",
"position",
"text_opacity",
"tint",
"scrollbar_color",
Expand Down Expand Up @@ -307,6 +310,9 @@ class StylesBase:
"""Set the margin (spacing outside the border) of the widget."""
offset = OffsetProperty()
"""Set the offset of the widget relative to where it would have been otherwise."""
position = StringEnumProperty(VALID_POSITION, "relative")
"""If `relative` offset is applied to widgets current position, if `absolute` it is applied to (0, 0)."""

border = BorderProperty(layout=True)
"""Set the border of the widget e.g. ("rounded", "green") or "none"."""

Expand Down Expand Up @@ -1003,6 +1009,8 @@ def append_declaration(name: str, value: str) -> None:
if "offset" in rules:
x, y = self.offset
append_declaration("offset", f"{x} {y}")
if "position" in rules:
append_declaration("position", self.position)
if "dock" in rules:
append_declaration("dock", rules["dock"])
if "split" in rules:
Expand Down
1 change: 1 addition & 0 deletions src/textual/css/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
TextAlign = Literal["left", "start", "center", "right", "end", "justify"]
Constrain = Literal["none", "inflect", "inside"]
Overlay = Literal["none", "screen"]
Position = Literal["relative", "absolute"]

Specificity3 = Tuple[int, int, int]
Specificity6 = Tuple[int, int, int, int, int, int]
Expand Down
8 changes: 8 additions & 0 deletions src/textual/demo/demo_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from textual.app import App
from textual.binding import Binding
from textual.demo.game import GameScreen
from textual.demo.home import HomeScreen
from textual.demo.projects import ProjectsScreen
from textual.demo.widgets import WidgetsScreen
Expand All @@ -26,6 +27,7 @@ class DemoApp(App):
"""

MODES = {
"game": GameScreen,
"home": HomeScreen,
"projects": ProjectsScreen,
"widgets": WidgetsScreen,
Expand All @@ -38,6 +40,12 @@ class DemoApp(App):
"home",
tooltip="Show the home screen",
),
Binding(
"g",
"app.switch_mode('game')",
"game",
tooltip="Unwind with a Textual game",
),
Binding(
"p",
"app.switch_mode('projects')",
Expand Down
Loading

0 comments on commit de9897b

Please sign in to comment.