From 09544e172f3b00c82394640edaf2a6d608ab96df Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 24 Nov 2021 21:26:27 +0000 Subject: [PATCH] key lines --- examples/basic.py | 42 ++++++++++----- src/textual/_border.py | 8 +-- src/textual/app.py | 32 +++++++----- src/textual/css/_style_properties.py | 21 ++++---- src/textual/css/_styles_builder.py | 12 ++--- src/textual/css/constants.py | 4 +- src/textual/css/scalar.py | 76 +++++++++++++++++++--------- src/textual/css/styles.py | 16 +++--- src/textual/css/stylesheet.py | 1 - src/textual/css/tokenize.py | 2 +- src/textual/layouts/dock.py | 3 -- src/textual/message.py | 3 ++ src/textual/message_pump.py | 31 +++++++----- src/textual/view.py | 1 + src/textual/widget.py | 38 +++++++------- src/textual/widgets/_placeholder.py | 1 - 16 files changed, 177 insertions(+), 114 deletions(-) diff --git a/examples/basic.py b/examples/basic.py index 2764bb323..16e837881 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,5 +1,5 @@ from textual.app import App -from textual.widgets import Placeholder +from textual.widget import Widget class BasicApp(App): @@ -9,35 +9,51 @@ class BasicApp(App): App > DockView { layout: dock; - docks: sidebar=left/1 widgets=top; + docks: side=left/1 header=top footer=bottom; layers: base panels; } #sidebar { - dock-group: sidebar; - width: 40; + text: bold #09312e on #3CAEA3; + /* dock-group: header; */ + width: 30; + height: 1fr; layer: panels; + border-right: vkey #09312e; } - #widget1 { - text: on blue; - dock-group: widgets; - height: 1fr; + #header { + text: on #173f5f; + dock-group: header; + height: 3; + border: hkey white; + } - #widget2 { - text: on red; - dock-group: widgets; - height: 1fr; + #footer { + dock-group: header; + height: 3; + border-top: hkey #0f2b41; + text: #3a3009 on #f6d55c; } + #content { + dock-group: header; + text: on #20639B; + } + + """ async def on_mount(self) -> None: """Build layout here.""" await self.view.mount( - sidebar=Placeholder(), widget1=Placeholder(), widget2=Placeholder() + header=Widget(), + content=Widget(), + footer=Widget(), + sidebar=Widget(), ) + self.panic(self.view.styles) BasicApp.run(log="textual.log") diff --git a/src/textual/_border.py b/src/textual/_border.py index 131e1fdd7..8fcaa4c39 100644 --- a/src/textual/_border.py +++ b/src/textual/_border.py @@ -17,6 +17,8 @@ BORDER_STYLES: dict[str, tuple[str, str, str]] = { "heavy": ("┏━┓", "┃ ┃", "┗━┛"), "inner": ("▗▄▖", "▐ ▌", "▝▀▘"), "outer": ("▛▀▜", "▌ ▐", "▙▄▟"), + "hkey": ("▔▔▔", " ", "▁▁▁"), + "vkey": ("▏ ▕", "▏ ▕", "▏ ▕"), } @@ -98,9 +100,9 @@ class Border: render_options = options.update_dimensions(width, height) lines = console.render_lines(self.renderable, render_options) - if len(lines) <= 2: - yield SegmentLines(lines, new_lines=True) - return + # if len(lines) <= 2: + # yield SegmentLines(lines, new_lines=True) + # return if self.outline: self._crop_renderable(lines, options.max_width) diff --git a/src/textual/app.py b/src/textual/app.py index f1878c162..3c7b7ff29 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -305,7 +305,6 @@ class App(DOMNode): self.stylesheet.read(self.css_file) if self.css is not None: self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>") - print(self.stylesheet.css) except StylesheetParseError as error: self.panic(error) self._print_error_renderables() @@ -322,16 +321,14 @@ class App(DOMNode): # Wait for the load event to be processed, so we don't go in to application mode beforehand await load_event.wait() - await self.post_message(events.Mount(sender=self)) - - view = DockView() - await self.mount(self, view) - await self.push_view(view) - driver = self._driver = self.driver_class(self.console, self) - driver.start_application_mode() + try: + mount_event = events.Mount(sender=self) + await self.dispatch_message(mount_event) + await mount_event.wait() + self.title = self._title self.refresh() await self.animator.start() @@ -339,8 +336,6 @@ class App(DOMNode): log("PROCESS END") await self.animator.stop() await self.close_all() - except Exception: - self.panic() finally: driver.stop_application_mode() except: @@ -376,13 +371,20 @@ class App(DOMNode): name_widgets: Iterable[tuple[str | None, Widget]] name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()] apply_stylesheet = self.stylesheet.apply + widget_events = [] for widget_id, widget in name_widgets: if widget not in self.registry: if widget_id is not None: widget.id = widget_id self._register(parent, widget) apply_stylesheet(widget) - widget.post_message_no_wait(events.Mount(sender=parent)) + mount_event = events.Mount(sender=parent) + widget_events.append((widget, mount_event)) + # await widget.post_message(mount_event) + # await mount_event.wait() + for widget, event in widget_events: + widget.post_message_no_wait(event) + # await event.wait() def is_mounted(self, widget: Widget) -> bool: return widget in self.registry @@ -471,7 +473,13 @@ class App(DOMNode): async def on_event(self, event: events.Event) -> None: # Handle input events that haven't been forwarded # If the event has been forwaded it may have bubbled up back to the App - if isinstance(event, events.InputEvent) and not event.is_forwarded: + if isinstance(event, events.Mount): + view = DockView() + await self.mount(self, view) + await self.push_view(view) + await super().on_event(event) + + elif isinstance(event, events.InputEvent) and not event.is_forwarded: if isinstance(event, events.MouseEvent): # Record current mouse position on App self.mouse_position = Offset(event.x, event.y) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index e24b46a2d..e2f3c3484 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -6,7 +6,7 @@ import rich.repr from rich.color import Color from rich.style import Style -from .scalar import Scalar, ScalarParseError +from .scalar import get_symbols, UNIT_SYMBOL, Unit, Scalar, ScalarParseError from ..geometry import Offset, Spacing, SpacingDimensions from .constants import NULL_SPACING, VALID_EDGE from .errors import StyleTypeError, StyleValueError @@ -14,15 +14,16 @@ from ._error_tools import friendly_list if TYPE_CHECKING: from .styles import Styles - from .styles import DockSpecification + from .styles import DockGroup class ScalarProperty: - def __init__(self, units: set[str]) -> None: - self.units = units + def __init__(self, units: set[Unit] | None = None) -> None: + self.units: set[Unit] = units or {*UNIT_SYMBOL} super().__init__() def __set_name__(self, owner: Styles, name: str) -> None: + self.name = name self.internal_name = f"_rule_{name}" def __get__( @@ -38,7 +39,7 @@ class ScalarProperty: if value is None: new_value = None elif isinstance(value, float): - new_value = Scalar(value, "") + new_value = Scalar(value, Unit.CELLS) elif isinstance(value, Scalar): new_value = value elif isinstance(value, str): @@ -49,7 +50,9 @@ class ScalarProperty: else: raise StyleValueError("expected float, Scalar, or None") if new_value is not None and new_value.unit not in self.units: - raise StyleValueError(f"units must be one of {friendly_list(self.units)}") + raise StyleValueError( + f"{self.name} units must be one of {friendly_list(get_symbols(self.units))}" + ) setattr(obj, self.internal_name, new_value) return value @@ -224,12 +227,12 @@ class SpacingProperty: class DocksProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None - ) -> tuple[DockSpecification, ...]: + ) -> tuple[DockGroup, ...]: return obj._rule_docks or () def __set__( - self, obj: Styles, docks: Iterable[DockSpecification] | None - ) -> Iterable[DockSpecification] | None: + self, obj: Styles, docks: Iterable[DockGroup] | None + ) -> Iterable[DockGroup] | None: if docks is None: obj._rule_docks = None else: diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index eb6c03d41..e05682394 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -12,7 +12,7 @@ from ._error_tools import friendly_list from ..geometry import Offset, Spacing, SpacingDimensions from .model import Declaration from .scalar import Scalar -from .styles import DockSpecification, Styles +from .styles import DockGroup, Styles from .types import Edge, Display, Visibility from .tokenize import Token @@ -138,7 +138,7 @@ class StylesBuilder: style_tokens: list[str] = [] append = style_tokens.append for token in tokens: - _, _, location, token_name, value = token + token_name, value, _, _, _ = token if token_name == "token": if value in VALID_BORDER: border_type = value @@ -157,7 +157,7 @@ class StylesBuilder: def _process_border(self, edge: str, name: str, tokens: list[Token]) -> None: border = self._parse_border("border", tokens) - setattr(self.styles, f"_border_{edge}", border) + setattr(self.styles, f"_rule_border_{edge}", border) def process_border(self, name: str, tokens: list[Token]) -> None: border = self._parse_border("border", tokens) @@ -179,7 +179,7 @@ class StylesBuilder: def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None: border = self._parse_border("outline", tokens) - setattr(self.styles, f"_outline_{edge}", border) + setattr(self.styles, f"_rule_outline_{edge}", border) def process_outline(self, name: str, tokens: list[Token]) -> None: border = self._parse_border("outline", tokens) @@ -289,7 +289,7 @@ class StylesBuilder: self.styles._rule_dock_group = tokens[0].value if tokens else "" def process_docks(self, name: str, tokens: list[Token]) -> None: - docks: list[DockSpecification] = [] + docks: list[DockGroup] = [] for token in tokens: if token.name == "key_value": key, edge_name = token.value.split("=") @@ -308,7 +308,7 @@ class StylesBuilder: token, f"edge must be one of 'top', 'right', 'bottom', or 'left'; found {edge_name!r}", ) - docks.append(DockSpecification(key.strip(), cast(Edge, edge_name), z)) + docks.append(DockGroup(key.strip(), cast(Edge, edge_name), z)) elif token.name == "bar": pass else: diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index e50ee25ab..5a8ea8e17 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -10,13 +10,15 @@ from ..geometry import Spacing VALID_VISIBILITY: Final = {"visible", "hidden"} VALID_DISPLAY: Final = {"block", "none"} VALID_BORDER: Final = { - "rounded", + "none" "round", "solid", "double", "dashed", "heavy", "inner", "outer", + "hkey", + "vkey", } VALID_EDGE: Final = {"top", "right", "bottom", "left"} VALID_LAYOUT: Final = {"dock", "vertical", "grid"} diff --git a/src/textual/css/scalar.py b/src/textual/css/scalar.py index 4caf10235..e269af4c0 100644 --- a/src/textual/css/scalar.py +++ b/src/textual/css/scalar.py @@ -1,10 +1,46 @@ from __future__ import annotations +from enum import Enum, unique import re -from typing import NamedTuple +from typing import Iterable, NamedTuple -_MATCH_SCALAR = re.compile(r"^(\d+\.?\d*)(fr|%)?$").match +@unique +class Unit(Enum): + CELLS = 1 + FRACTION = 2 + PERCENT = 3 + WIDTH = 4 + HEIGHT = 5 + VIEW_WIDTH = 6 + VIEW_HEIGHT = 7 + + +UNIT_SYMBOL = { + Unit.CELLS: "", + Unit.FRACTION: "fr", + Unit.PERCENT: "%", + Unit.WIDTH: "w", + Unit.HEIGHT: "h", + Unit.VIEW_WIDTH: "vw", + Unit.VIEW_HEIGHT: "vh", +} + +SYMBOL_UNIT = {v: k for k, v in UNIT_SYMBOL.items()} + +_MATCH_SCALAR = re.compile(r"^(\d+\.?\d*)(fr|%|w|h|vw|vh)?$").match + + +def get_symbols(units: Iterable[Unit]) -> list[str]: + """Get symbols for an iterable of units. + + Args: + units (Iterable[Unit]): A number of units. + + Returns: + list[str]: List of symbols. + """ + return [UNIT_SYMBOL[unit] for unit in units] class ScalarParseError(Exception): @@ -15,36 +51,25 @@ class Scalar(NamedTuple): """A numeric value and a unit.""" value: float - unit: str + unit: Unit def __str__(self) -> str: - value, unit = self - return f"{int(value) if value.is_integer() else value}{unit}" + value, _unit = self + return f"{int(value) if value.is_integer() else value}{self.symbol}" @property def cells(self) -> int | None: value, unit = self - if unit: - return None - else: - return int(value) + return int(value) if unit == Unit.CELLS else None @property def fraction(self) -> int | None: value, unit = self - if unit == "fr": - return int(value) - else: - return None + return int(value) if unit == Unit.FRACTION else None - def resolve_size(self, total: int, total_fraction: int) -> int: - value, unit = self - if unit == "": - return int(value) - elif unit == "%": - return int(total * value / 100.0) - else: # if unit == "fr": - return int((value / total_fraction) * total) + @property + def symbol(self) -> str: + return UNIT_SYMBOL[self.unit] @classmethod def parse(cls, token: str) -> Scalar: @@ -62,11 +87,14 @@ class Scalar(NamedTuple): match = _MATCH_SCALAR(token) if match is None: raise ScalarParseError(f"{token!r} is not a valid scalar") - value, unit = match.groups() - scalar = cls(float(value), unit or "") + value, unit_name = match.groups() + scalar = cls(float(value), SYMBOL_UNIT[unit_name or ""]) return scalar if __name__ == "__main__": - print(Scalar.parse("3.14")) + print(Scalar.parse("3.14fr")) + s = Scalar.parse("23") + print(repr(s)) + print(repr(s.cells)) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index b49413bcb..9588680be 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -43,7 +43,7 @@ else: from typing_extensions import Literal -class DockSpecification(NamedTuple): +class DockGroup(NamedTuple): name: str edge: Edge z: int @@ -83,7 +83,7 @@ class Styles: _rule_layout: str | None = None _rule_dock_group: str | None = None - _rule_docks: tuple[DockSpecification, ...] | None = None + _rule_docks: tuple[DockGroup, ...] | None = None _rule_layers: tuple[str, ...] | None = None _rule_layer: str | None = None @@ -115,10 +115,10 @@ class Styles: outline_bottom = BoxProperty() outline_left = BoxProperty() - width = ScalarProperty({"", "fr"}) - height = ScalarProperty({"", "fr"}) - min_width = ScalarProperty({"", "fr"}) - min_height = ScalarProperty({"", "fr"}) + width = ScalarProperty() + height = ScalarProperty() + min_width = ScalarProperty() + min_height = ScalarProperty() dock_group = DockGroupProperty() docks = DocksProperty() @@ -251,8 +251,8 @@ class Styles: append_declaration( "docks", " ".join( - (f"{key}={value}/{z}" if z else f"{key}={value}") - for key, value, z in self._rule_docks + (f"{name}={edge}/{z}" if z else f"{name}={edge}") + for name, edge, z in self._rule_docks ), ) if self._rule_layers is not None: diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 36c8c12d2..a9ee34413 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -134,7 +134,6 @@ class Stylesheet: for name, specificity_rules in rule_attributes.items() ] node.styles.apply_rules(node_rules) - log(node, node_rules) if __name__ == "__main__": diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index 8e78d7317..146b9faf8 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -46,7 +46,7 @@ expect_declaration_content = Expect( comment_start=r"\/\*", percentage=r"\d+\%", scalar=r"\d+\.?\d*(?:fr|%)?", - color=r"\#[0-9a-f]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)", + color=r"\#[0-9a-fA-F]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)", key_value=r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+", token="[a-zA-Z_-]+", string=r"\".*?\"", diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index c3dde541a..4edf0f684 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -67,7 +67,6 @@ class DockLayout(Layout): self, view: View, size: Size, scroll: Offset ) -> Iterable[WidgetPlacement]: - map: LayoutMap = LayoutMap(size) width, height = size layout_region = Region(0, 0, width, height) layers: dict[int, Region] = defaultdict(lambda: layout_region) @@ -170,5 +169,3 @@ class DockLayout(Layout): region = Region(x, y, width - total, height) layers[z] = region - - return map diff --git a/src/textual/message.py b/src/textual/message.py index 7a5af02a9..510d3a3b8 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -52,6 +52,9 @@ class Message: cls.bubble = bubble cls.verbosity = verbosity + def set_done(self) -> None: + self._done_event.set() + @property def _done_event(self) -> Event: if self.__done_event is None: diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index f9e80db55..3ca6e8a28 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -225,14 +225,11 @@ class MessagePump: async def dispatch_message(self, message: Message) -> bool | None: _rich_traceback_guard = True - try: - if isinstance(message, events.Event): - if not isinstance(message, events.Null): - await self.on_event(message) - else: - return await self.on_message(message) - finally: - message._done_event.set() + if isinstance(message, events.Event): + if not isinstance(message, events.Null): + await self.on_event(message) + else: + return await self.on_message(message) return False def _get_dispatch_methods( @@ -248,9 +245,12 @@ class MessagePump: async def on_event(self, event: events.Event) -> None: _rich_traceback_guard = True - for method in self._get_dispatch_methods(f"on_{event.name}", event): - log(event, ">>>", self, verbosity=event.verbosity) - await invoke(method, event) + try: + for method in self._get_dispatch_methods(f"on_{event.name}", event): + log(event, ">>>", self, verbosity=event.verbosity) + await invoke(method, event) + finally: + event.set_done() if event.bubble and self._parent and not event._stop_propagation: if event.sender == self._parent: @@ -264,9 +264,12 @@ class MessagePump: method_name = f"handle_{message.name}" method = getattr(self, method_name, None) - if method is not None: - log(message, ">>>", self, verbosity=message.verbosity) - await invoke(method, message) + try: + if method is not None: + log(message, ">>>", self, verbosity=message.verbosity) + await invoke(method, message) + finally: + message.set_done() if message.bubble and self._parent and not message._stop_propagation: if message.sender == self._parent: diff --git a/src/textual/view.py b/src/textual/view.py index 048d6d920..fb56d2668 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -118,6 +118,7 @@ class View(Widget): if cached_size == size and cached_scroll == scroll: return arrangement arrangement = list(self._layout.arrange(self, size, scroll)) + self.log(arrangement) self._cached_arrangement = (size, scroll, arrangement) return arrangement diff --git a/src/textual/widget.py b/src/textual/widget.py index 4ed35eca9..e46bc66a5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -19,7 +19,7 @@ from rich.padding import Padding from rich.pretty import Pretty from rich.style import Style from rich.styled import Styled -from rich.text import TextType +from rich.text import Text, TextType from . import events from . import errors @@ -135,24 +135,28 @@ class Widget(DOMNode): Returns: RenderableType: A new renderable. """ - renderable = Styled(self.render(), self.styles.text) + + renderable = self.render() + styles = self.styles if self.padding is not None: renderable = Padding(renderable, self.padding) - if self.border not in ("", "none"): - _border_style = self.console.get_style(self.border_style) - renderable = Border( - renderable, - ( - ("heavy", _border_style), - ("heavy", _border_style), - ("heavy", _border_style), - ("heavy", _border_style), - ), - ) + + if styles.has_border: + renderable = Border(renderable, styles.border) + + # _border_style = self.console.get_style(self.border_style) + # renderable = Border( + # renderable, + # ( + # ("heavy", _border_style), + # ("heavy", _border_style), + # ("heavy", _border_style), + # ("heavy", _border_style), + # ), + # ) if self.margin is not None: renderable = Padding(renderable, self.margin) - if self.style: - renderable = Styled(renderable, self.style) + renderable = Styled(renderable, styles.text) return renderable @property @@ -269,9 +273,7 @@ class Widget(DOMNode): Returns: RenderableType: Any renderable """ - return Panel( - Align.center(Pretty(self), vertical="middle"), title=self.__class__.__name__ - ) + return Align.center(Text(f"#{self.id}"), vertical="middle") async def action(self, action: str, *params) -> None: await self.app.action(action, self) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 034861ea4..a0946b4fc 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -10,7 +10,6 @@ import rich.repr from logging import getLogger from .. import events -from ..geometry import Offset from ..widget import Reactive, Widget log = getLogger("rich")