Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative compose #1847

Merged
merged 12 commits into from
Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Changed

- Added alternative method of composing Widgets
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved

## [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
39 changes: 39 additions & 0 deletions src/textual/_compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
nodes: list[Widget] = []
compose_stack: list[Widget] = []
composed: list[Widget] = []
app._compose_stacks.append(compose_stack)
app._composed.append(composed)
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
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()
return nodes
6 changes: 5 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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 @@ -388,6 +389,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 @@ -1606,7 +1610,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
20 changes: 19 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 @@ -38,6 +39,7 @@
from ._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction
from ._arrange import DockArrangeResult, arrange
from ._asyncio import create_task
from ._compose import compose
from ._context import active_app
from ._easing import DEFAULT_SCROLL_EASING
from ._layout import Layout
Expand Down Expand Up @@ -363,6 +365,22 @@ def offset(self) -> Offset:
def offset(self, offset: Offset) -> None:
self.styles.offset = ScalarOffset.from_offset(offset)

def __enter__(self) -> None:
willmcgugan marked this conversation as resolved.
Show resolved Hide resolved
self.app._compose_stacks[-1].append(self)

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
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 @@ -2444,7 +2462,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