Skip to content

Commit

Permalink
Merge pull request #1847 from Textualize/alt-compose
Browse files Browse the repository at this point in the history
Alternative compose
  • Loading branch information
willmcgugan authored Feb 21, 2023
2 parents ab3e523 + a714ffe commit f0e454f
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 32 deletions.
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Added `App.batch_update` https://github.com/Textualize/textual/pull/1832
- Added horizontal rule to Markdown https://github.com/Textualize/textual/pull/1832
- Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785

### Changed

- Scrolling by page now adds to current position.
- Markdown lists have been polished: a selection of bullets, better alignment of numbers, style tweaks https://github.com/Textualize/textual/pull/1832
- Added alternative method of composing Widgets https://github.com/Textualize/textual/pull/1847

### Removed

- Removed `screen.visible_widgets` and `screen.widgets`

### Added

- Added `Widget.disabled` https://github.com/Textualize/textual/pull/1785

### Fixed

- Numbers in a descendant-combined selector no longer cause an error https://github.com/Textualize/textual/issues/1836


## [0.11.1] - 2023-02-17

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions examples/code_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ def compose(self) -> ComposeResult:
"""Compose our UI."""
path = "./" if len(sys.argv) < 2 else sys.argv[1]
yield Header()
yield Container(
DirectoryTree(path, id="tree-view"),
Vertical(Static(id="code", expand=True), id="code-view"),
)
with Container():
yield DirectoryTree(path, id="tree-view")
with Vertical(id="code-view"):
yield Static(id="code", expand=True)
yield Footer()

def on_mount(self, event: events.Mount) -> None:
Expand Down
3 changes: 2 additions & 1 deletion examples/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ class DictionaryApp(App):

def compose(self) -> ComposeResult:
yield Input(placeholder="Search for a word")
yield Content(Markdown(id="results"), id="results-container")
with Content(id="results-container"):
yield Static(id="results")

def on_mount(self) -> None:
"""Called when app starts."""
Expand Down
40 changes: 40 additions & 0 deletions src/textual/_compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from .app import App
from .widget import Widget


def compose(node: App | Widget) -> list[Widget]:
"""Compose child widgets.
Args:
node: The parent node.
Returns:
A list of widgets.
"""
app = node.app
nodes: list[Widget] = []
compose_stack: list[Widget] = []
composed: list[Widget] = []
app._compose_stacks.append(compose_stack)
app._composed.append(composed)
try:
for child in node.compose():
if composed:
nodes.extend(composed)
composed.clear()
if compose_stack:
compose_stack[-1]._nodes._append(child)
else:
nodes.append(child)
if composed:
nodes.extend(composed)
composed.clear()
finally:
app._compose_stacks.pop()
app._composed.pop()
return nodes
6 changes: 5 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from ._ansi_sequences import SYNC_END, SYNC_START
from ._asyncio import create_task
from ._callback import invoke
from ._compose import compose
from ._context import active_app
from ._event_broker import NoHandler, extract_handler_actions
from ._path import _make_path_object_relative
Expand Down Expand Up @@ -399,6 +400,9 @@ def __init__(
self._installed_screens: dict[str, Screen | Callable[[], Screen]] = {}
self._installed_screens.update(**self.SCREENS)

self._compose_stacks: list[list[Widget]] = []
self._composed: list[list[Widget]] = []

self.devtools: DevtoolsClient | None = None
if "devtools" in self.features:
try:
Expand Down Expand Up @@ -1643,7 +1647,7 @@ async def on_screenshot():

async def _on_compose(self) -> None:
try:
widgets = list(self.compose())
widgets = compose(self)
except TypeError as error:
raise TypeError(
f"{self!r} compose() returned an invalid response; {error}"
Expand Down
20 changes: 8 additions & 12 deletions src/textual/cli/previews/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,14 @@ def compose(self) -> ComposeResult:
]

for color_name in ColorSystem.COLOR_NAMES:
items: list[Widget] = [Label(f'"{color_name}"')]
for level in LEVELS:
color = f"{color_name}-{level}" if level else color_name
item = ColorItem(
ColorBar(f"${color}", classes="text label"),
ColorBar("$text-muted", classes="muted"),
ColorBar("$text-disabled", classes="disabled"),
classes=color,
)
items.append(item)

yield ColorGroup(*items, id=f"group-{color_name}")
with ColorGroup(id=f"group-{color_name}"):
yield Label(f'"{color_name}"')
for level in LEVELS:
color = f"{color_name}-{level}" if level else color_name
with ColorItem(classes=color):
yield ColorBar(f"${color}", classes="text label")
yield ColorBar("$text-muted", classes="muted")
yield ColorBar("$text-disabled", classes="disabled")


class ColorsApp(App):
Expand Down
18 changes: 8 additions & 10 deletions src/textual/cli/previews/easing.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,14 @@ def compose(self) -> ComposeResult:
)

yield EasingButtons()
yield Vertical(
Horizontal(
Label("Animation Duration:", id="label"), duration_input, id="inputs"
),
Horizontal(
self.animated_bar,
Container(self.opacity_widget, id="other"),
),
Footer(),
)
with Vertical():
with Horizontal(id="inputs"):
yield Label("Animation Duration:", id="label")
yield duration_input
with Horizontal():
yield self.animated_bar
yield Container(self.opacity_widget, id="other")
yield Footer()

def on_button_pressed(self, event: Button.Pressed) -> None:
self.bell()
Expand Down
24 changes: 23 additions & 1 deletion src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from fractions import Fraction
from itertools import islice
from operator import attrgetter
from types import TracebackType
from typing import (
TYPE_CHECKING,
ClassVar,
Expand Down Expand Up @@ -33,11 +34,13 @@
from rich.style import Style
from rich.text import Text
from rich.traceback import Traceback
from typing_extensions import Self

from . import errors, events, messages
from ._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction
from ._arrange import DockArrangeResult, arrange
from ._asyncio import create_task
from ._compose import compose
from ._cache import FIFOCache
from ._context import active_app
from ._easing import DEFAULT_SCROLL_EASING
Expand Down Expand Up @@ -370,6 +373,25 @@ def offset(self) -> Offset:
def offset(self, offset: Offset) -> None:
self.styles.offset = ScalarOffset.from_offset(offset)

def __enter__(self) -> Self:
"""Use as context manager when composing."""
self.app._compose_stacks[-1].append(self)
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Exit compose context manager."""
compose_stack = self.app._compose_stacks[-1]
composed = compose_stack.pop()
if compose_stack:
compose_stack[-1]._nodes._append(composed)
else:
self.app._composed[-1].append(composed)

ExpectType = TypeVar("ExpectType", bound="Widget")

@overload
Expand Down Expand Up @@ -2497,7 +2519,7 @@ async def handle_key(self, event: events.Key) -> bool:

async def _on_compose(self) -> None:
try:
widgets = list(self.compose())
widgets = compose(self)
except TypeError as error:
raise TypeError(
f"{self!r} compose() returned an invalid response; {error}"
Expand Down

0 comments on commit f0e454f

Please sign in to comment.