diff --git a/examples/basic.py b/examples/basic.py new file mode 100644 index 000000000..4aa63ed49 --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,31 @@ +from textual.app import App +from textual.widgets import Placeholder + + +class BasicApp(App): + """Demonstrates smooth animation. Press 'b' to see it in action.""" + + css = """ + + App > View { + layout: dock + } + + #widget1 { + edge: top + } + + #widget2 { + + + } + + """ + + async def on_mount(self) -> None: + """Build layout here.""" + + await self.view.mount(widget1=Placeholder(), widget2=Placeholder()) + + +SmoothApp.run(log="textual.log") diff --git a/examples/simple.py b/examples/simple.py index ac621befe..b2e8cbb4a 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,6 +1,5 @@ from rich.markdown import Markdown -from textual import events from textual.app import App from textual.widgets import Header, Footer, Placeholder, ScrollView @@ -8,24 +7,38 @@ from textual.widgets import Header, Footer, Placeholder, ScrollView class MyApp(App): """An example of a very simple Textual App""" - async def on_load(self, event: events.Load) -> None: + stylesheet = """ + + App > View { + layout: dock + } + + #body { + padding: 1 + } + + #sidebar { + edge left + size: 40 + } + + """ + + async def on_load(self) -> None: """Bind keys with the app loads (but before entering application mode)""" await self.bind("b", "view.toggle('sidebar')", "Toggle sidebar") await self.bind("q", "quit", "Quit") - async def on_mount(self, event: events.Mount) -> None: + async def on_mount(self) -> None: """Create and dock the widgets.""" - # A scrollview to contain the markdown file - body = ScrollView(gutter=1) - - # Header / footer / dock - await self.view.dock(Header(), edge="top") - await self.view.dock(Footer(), edge="bottom") - await self.view.dock(Placeholder(), edge="left", size=30, name="sidebar") - - # Dock the body in the remaining space - await self.view.dock(body, edge="right") + body = ScrollView() + await self.view.mount( + Header(), + Footer(), + body=body, + sidebar=Placeholder(), + ) async def get_markdown(filename: str) -> None: with open(filename, "rt") as fh: @@ -35,4 +48,4 @@ class MyApp(App): await self.call_later(get_markdown, "richreadme.md") -MyApp.run(title="Simple App", log="textual.log", css_file="theme.css") +MyApp.run(title="Simple App", log="textual.log") diff --git a/src/textual/app.py b/src/textual/app.py index 71fc69a5f..cb928e0e7 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -71,6 +71,7 @@ class App(MessagePump): log_verbosity: int = 1, title: str = "Textual Application", css_file: str | None = None, + css: str | None = None, ): """The Textual Application base class @@ -112,6 +113,7 @@ class App(MessagePump): self.stylesheet = Stylesheet() self.css_file = css_file + self.css = css super().__init__() @@ -299,7 +301,8 @@ class App(MessagePump): try: if self.css_file is not None: self.stylesheet.read(self.css_file) - print(self.stylesheet.css) + if self.css is not None: + self.stylesheet.parse(self.css) except Exception: self.panic() diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index e6ba19fbe..152016288 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from .styles import Styles -class _BoxProperty: +class BoxProperty: DEFAULT = ("", Style()) @@ -67,7 +67,7 @@ class Edges(NamedTuple): yield "left", left -class _BorderProperty: +class BorderProperty: def __set_name__(self, owner: Styles, name: str) -> None: self._properties = ( f"{name}_top", @@ -129,14 +129,21 @@ class _BorderProperty: raise StyleValueError("expected 1, 2, or 4 values") -class _StyleProperty: +class StyleProperty: + + DEFAULT_STYLE = Style() + def __set_name__(self, owner: Styles, name: str) -> None: self._internal_name = f"_{name}" def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Style: - return getattr(obj, self._internal_name) + return getattr(obj, self._internal_name) or self.DEFAULT_STYLE + + def __set__(self, obj: Styles, style: Style | str | None) -> Style | str | None: + if style is None: + setattr(obj, self._internal_name, None) + return None - def __set__(self, obj: Styles, style: Style | str) -> Style: if isinstance(style, str): _style = Style.parse(style) else: @@ -145,7 +152,7 @@ class _StyleProperty: return _style -class _SpacingProperty: +class SpacingProperty: def __set_name__(self, owner: Styles, name: str) -> None: self._internal_name = f"_{name}" @@ -158,7 +165,7 @@ class _SpacingProperty: return spacing -class _DocksProperty: +class DocksProperty: def __get__( self, obj: Styles, objtype: type[Styles] | None = None ) -> tuple[str, ...]: @@ -173,7 +180,7 @@ class _DocksProperty: return _docks -class _DockGroupProperty: +class DockGroupProperty: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: return obj._dock_group or "" @@ -182,7 +189,7 @@ class _DockGroupProperty: return spacing -class _OffsetProperty: +class OffsetProperty: def __set_name__(self, owner: Styles, name: str) -> None: self._internal_name = f"_{name}" @@ -195,7 +202,7 @@ class _OffsetProperty: return offset -class _DockEdgeProperty: +class DockEdgeProperty: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: return obj._dock_edge or "" @@ -204,3 +211,37 @@ class _DockEdgeProperty: raise ValueError(f"dock edge must be one of {friendly_list(VALID_EDGE)}") obj._dock_edge = edge return edge + + +class IntegerProperty: + def __set_name__(self, owner: Styles, name: str) -> None: + self._internal_name = f"_{name}" + + def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> int: + return getattr(obj, self._internal_name, 0) + + def __set__(self, obj: Styles, value: int | None) -> int | None: + setattr(obj, self._internal_name, value) + return value + + +class StringProperty: + def __init__(self, valid_values: set[str], default: str) -> None: + self._valid_values = valid_values + self._default = default + + def __set_name__(self, owner: Styles, name: str) -> None: + self._name = name + self._internal_name = f"_{name}" + + def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: + return getattr(obj, self._internal_name, self._default) + + def __set__(self, obj: Styles, value: str | None = None) -> str | None: + if value is not None: + if value not in self._valid_values: + raise StyleValueError( + f"{self._name} must be one of {friendly_list(self._valid_values)}" + ) + setattr(obj, self._internal_name, value) + return value diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index ced9b0277..29be4b04b 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -16,14 +16,16 @@ from .constants import ( ) from ..geometry import NULL_OFFSET, Offset, Spacing from ._style_properties import ( - _BorderProperty, - _BoxProperty, - _DockEdgeProperty, - _DocksProperty, - _DockGroupProperty, - _OffsetProperty, - _SpacingProperty, - _StyleProperty, + BorderProperty, + BoxProperty, + DockEdgeProperty, + DocksProperty, + DockGroupProperty, + IntegerProperty, + OffsetProperty, + SpacingProperty, + StringProperty, + StyleProperty, ) from .types import Display, Visibility @@ -33,10 +35,10 @@ class Styles: _display: Display | None = None _visibility: Visibility | None = None + _layout: str | None = None - _text: Style = Style() + _text: Style | None = None - _layout: str = "" _padding: Spacing | None = None _margin: Spacing | None = None _offset: Offset | None = None @@ -51,71 +53,45 @@ class Styles: _outline_bottom: tuple[str, Style] | None = None _outline_left: tuple[str, Style] | None = None + _size: int | None = None + _fraction: int | None = None + _min_size: int | None = None + _dock_group: str | None = None _dock_edge: str | None = None _docks: tuple[str, ...] | None = None important: set[str] = field(default_factory=set) - @property - def display(self) -> Display: - return self._display or "block" + display = StringProperty(VALID_DISPLAY, "block") + visibility = StringProperty(VALID_VISIBILITY, "visible") + layout = StringProperty(VALID_LAYOUT, "dock") - @display.setter - def display(self, display: Display) -> None: - if display not in VALID_DISPLAY: - raise StyleValueError( - f"display must be one of {friendly_list(VALID_DISPLAY)}" - ) - self._display = display + text = StyleProperty() - @property - def visibility(self) -> Visibility: - return self._visibility or "visible" + padding = SpacingProperty() + margin = SpacingProperty() + offset = OffsetProperty() - @visibility.setter - def visibility(self, visibility: Visibility) -> None: - if visibility not in VALID_VISIBILITY: - raise StyleValueError( - f"visibility must be one of {friendly_list(VALID_VISIBILITY)}" - ) - self._visibility = visibility + border = BorderProperty() + border_top = BoxProperty() + border_right = BoxProperty() + border_bottom = BoxProperty() + border_left = BoxProperty() - text = _StyleProperty() + outline = BorderProperty() + outline_top = BoxProperty() + outline_right = BoxProperty() + outline_bottom = BoxProperty() + outline_left = BoxProperty() - @property - def layout(self) -> str: - return self._layout + size = IntegerProperty() + fraction = IntegerProperty() + min_size = IntegerProperty() - @layout.setter - def layout(self, layout: str) -> None: - if layout not in VALID_LAYOUT: - raise StyleValueError( - f"layout must be one of {friendly_list(VALID_LAYOUT)}" - ) - self._layout = layout - - offset = _OffsetProperty() - - padding = _SpacingProperty() - margin = _SpacingProperty() - - border = _BorderProperty() - border_top = _BoxProperty() - border_right = _BoxProperty() - border_bottom = _BoxProperty() - border_left = _BoxProperty() - - outline = _BorderProperty() - outline_top = _BoxProperty() - outline_right = _BoxProperty() - outline_bottom = _BoxProperty() - outline_left = _BoxProperty() - - dock_group = _DockGroupProperty() - docks = _DocksProperty() - - dock_edge = _DockEdgeProperty() + dock_group = DockGroupProperty() + docks = DocksProperty() + dock_edge = DockEdgeProperty() @property def has_border(self) -> bool: @@ -238,7 +214,6 @@ if __name__ == "__main__": styles.docks = "foo bar" styles.text = "italic blue" styles.dock_group = "bar" - styles.dock_edge = "sdfsdf" from rich import inspect, print diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index 5d14732d0..1a73482f4 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -41,10 +41,6 @@ class Dock: class DockLayout(Layout): - def __init__(self, docks: list[Dock] = None) -> None: - self.docks: list[Dock] = docks or [] - super().__init__() - def get_widgets(self) -> Iterable[Widget]: for dock in self.docks: yield from dock.widgets diff --git a/src/textual/view.py b/src/textual/view.py index 6be58450b..80a5532c5 100644 --- a/src/textual/view.py +++ b/src/textual/view.py @@ -41,7 +41,9 @@ class View(Widget): layout_factory: ClassVar[Callable[[], Layout]] - def __init__(self, layout: Layout = None, name: str | None = None) -> None: + def __init__( + self, layout: Layout = None, name: str | None = None, id: str | None = None + ) -> None: self._layout: Layout = layout or self.layout_factory() self.mouse_over: Widget | None = None @@ -55,7 +57,7 @@ class View(Widget): [], ) - super().__init__(name=name) + super().__init__(name=name, id=id) def __init_subclass__( cls, layout: Callable[[], Layout] | None = None, **kwargs @@ -144,7 +146,7 @@ class View(Widget): for name, widget in name_widgets: if name is not None: widget.name = name - self.add_child(widget) + self._add_child(widget) self.refresh() diff --git a/src/textual/views/_dock_view.py b/src/textual/views/_dock_view.py index c4a4ccca7..842dae397 100644 --- a/src/textual/views/_dock_view.py +++ b/src/textual/views/_dock_view.py @@ -21,16 +21,21 @@ class DockView(View): async def dock( self, *widgets: Widget, + name: str | None = None, + id: str | None = None, edge: DockEdge = "top", z: int = 0, size: int | None | DoNotSet = do_not_set, - name: str | None = None, ) -> None: dock = Dock(edge, widgets, z) assert isinstance(self._layout, DockLayout) self._layout.docks.append(dock) for widget in widgets: + if id is not None: + widget._id = id + if name is not None: + widget.name = name if size is not do_not_set: widget.layout_size = cast(Optional[int], size) if name is None: @@ -42,17 +47,18 @@ class DockView(View): async def dock_grid( self, *, + name: str | None = None, + id: str | None = None, edge: DockEdge = "top", z: int = 0, size: int | None | DoNotSet = do_not_set, - name: str | None = None, gap: tuple[int, int] | int | None = None, gutter: tuple[int, int] | int | None = None, align: tuple[GridAlign, GridAlign] | None = None, ) -> GridLayout: grid = GridLayout(gap=gap, gutter=gutter, align=align) - view = View(layout=grid, name=name) + view = View(layout=grid, id=id, name=name) dock = Dock(edge, (view,), z) assert isinstance(self._layout, DockLayout) self._layout.docks.append(dock) diff --git a/src/textual/widget.py b/src/textual/widget.py index 5fb6220ad..cb741ce30 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -85,7 +85,6 @@ class Widget(MessagePump): super().__init__() - id: str | None = None visible: Reactive[bool] = Reactive(True, layout=True) layout_size: Reactive[int | None] = Reactive(None, layout=True) layout_fraction: Reactive[int] = Reactive(1, layout=True) @@ -124,7 +123,7 @@ class Widget(MessagePump): renderable = self.render_styled() return renderable - def add_child(self, widget: Widget) -> Widget: + def _add_child(self, widget: Widget) -> Widget: """Add a child widget. Args: @@ -134,12 +133,21 @@ class Widget(MessagePump): self.children._append(widget) return widget - def get_child(self, name: str | None = None) -> Widget: - for widget in self.children: - if widget.name == name: - return widget + def get_child(self, name: str | None = None, id: str | None = None) -> Widget: + if name is not None: + for widget in self.children: + if widget.name == name: + return widget + if id is not None: + for widget in self.children: + if widget.id == id: + return widget raise errors.MissingWidget(f"Widget named {name!r} was not found in {self}") + @property + def id(self) -> str | None: + return self._id + @property def class_names(self) -> frozenset[str]: return frozenset(self._class_names) @@ -188,6 +196,7 @@ class Widget(MessagePump): if self.padding is not None: renderable = Padding(renderable, self.padding) if self.border not in ("", "none"): + 1 / 0 _border_style = self.console.get_style(self.border_style) renderable = Border( renderable, diff --git a/src/textual/widgets/_header.py b/src/textual/widgets/_header.py index a5f339de9..643dc9e82 100644 --- a/src/textual/widgets/_header.py +++ b/src/textual/widgets/_header.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import datetime from logging import getLogger diff --git a/src/textual/widgets/_scroll_view.py b/src/textual/widgets/_scroll_view.py index 562609a81..210edf740 100644 --- a/src/textual/widgets/_scroll_view.py +++ b/src/textual/widgets/_scroll_view.py @@ -35,14 +35,12 @@ class ScrollView(View): super().__init__(name=name, layout=layout) self.fluid = fluid - self.vscroll = self.add_child(ScrollBar(vertical=True)) - self.hscroll = self.add_child(ScrollBar(vertical=False)) - self.window = self.add_child( - WindowView( - "" if contents is None else contents, - auto_width=auto_width, - gutter=gutter, - ) + self.vscroll = ScrollBar(vertical=True) + self.hscroll = ScrollBar(vertical=False) + self.window = WindowView( + "" if contents is None else contents, + auto_width=auto_width, + gutter=gutter, ) layout.add_column("main")