From e761e7ae8fc625a913f6b40b75bc691e4743644d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 18 Aug 2022 16:05:11 +0100 Subject: [PATCH] timer example --- docs/examples/introduction/timers.css | 55 ++++++++++++++++++ docs/examples/introduction/timers.py | 84 +++++++++++++++++++++++++++ src/textual/_arrange.py | 16 +++-- src/textual/dom.py | 2 +- src/textual/geometry.py | 4 +- src/textual/layouts/horizontal.py | 4 +- src/textual/widget.py | 2 +- 7 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 docs/examples/introduction/timers.css create mode 100644 docs/examples/introduction/timers.py diff --git a/docs/examples/introduction/timers.css b/docs/examples/introduction/timers.css new file mode 100644 index 000000000..e4188de60 --- /dev/null +++ b/docs/examples/introduction/timers.css @@ -0,0 +1,55 @@ +TimerWidget { + layout: horizontal; + height: 5; + background: $panel-darken-1; + border: tall $panel-darken-2; + margin: 1; + padding: 0 1; + + transition: background 200ms linear; +} + +TimeDisplay { + content-align: center middle; + opacity: 60%; + height: 3; +} + +Button { + width: 16; +} + +#start { + dock: left; +} + +TimerWidget.started { + opacity: 100%; + text-style: bold; + background: $success; + color: $text-success; + border: tall $success-darken-2; +} + +TimerWidget.started #start { + display: none +} + +TimerWidget.started #stop { + display: block +} + +TimerWidget.started #reset { + visibility: hidden +} + + +#stop { + dock: left; + display: none; +} + + +Button#reset { + dock: right; +} diff --git a/docs/examples/introduction/timers.py b/docs/examples/introduction/timers.py new file mode 100644 index 000000000..9ae350b68 --- /dev/null +++ b/docs/examples/introduction/timers.py @@ -0,0 +1,84 @@ +from time import time + +from textual.app import App, ComposeResult +from textual.layout import Container +from textual.reactive import Reactive +from textual.widgets import Button, Header, Footer, Static + + +class TimeDisplay(Static): + """Displays the time.""" + + time_delta = Reactive(0.0) + + def watch_time_delta(self, time_delta: float) -> None: + minutes, seconds = divmod(time_delta, 60) + hours, minutes = divmod(minutes, 60) + self.update(f"{hours:02.0f}:{minutes:02.0f}:{seconds:02.2f}") + + +class TimerWidget(Static): + """The timer widget (display + buttons).""" + + start_time = Reactive(0.0) + total = Reactive(0.0) + started = Reactive(False) + + def on_mount(self) -> None: + self.update_timer = self.set_interval(1 / 30, self.update_elapsed, pause=True) + + def update_elapsed(self) -> None: + time_delta = ( + self.total + time() - self.start_time if self.started else self.total + ) + self.query_one(TimeDisplay).time_delta = time_delta + + def compose(self) -> ComposeResult: + yield Button("Start", id="start", variant="success") + yield Button("Stop", id="stop", variant="error") + yield TimeDisplay() + yield Button("Reset", id="reset") + + def watch_started(self, started: bool) -> None: + if started: + self.start_time = time() + self.update_timer.resume() + self.add_class("started") + else: + self.update_timer.pause() + self.total += time() - self.start_time + self.remove_class("started") + + def on_button_pressed(self, event: Button.Pressed) -> None: + button_id = event.button.id + self.started = button_id == "start" + if button_id == "reset": + self.started = False + self.total = 0.0 + self.update_elapsed() + + +class TimerApp(App): + def on_load(self) -> None: + self.bind("a", "add_timer", description="Add") + self.bind("r", "remove_timer", description="Remove") + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Container(TimerWidget(), TimerWidget(), TimerWidget()) + + def action_add_timer(self) -> None: + new_timer = TimerWidget() + self.query_one("Container").mount(new_timer) + self.call_later(new_timer.scroll_visible) + + def action_remove_timer(self) -> None: + timers = self.query("Container TimerWidget") + if timers: + timers.last().remove() + + +app = TimerApp(css_path="timers.css") +if __name__ == "__main__": + app.run() diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 6e133b1d3..984a32c28 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -54,6 +54,7 @@ def arrange( for widgets in dock_layers.values(): layout_widgets, dock_widgets = partition(get_dock, widgets) + arrange_widgets.update(dock_widgets) top = right = bottom = left = 0 @@ -73,18 +74,18 @@ def arrange( dock_region = Region( 0, height - widget_height, widget_width, widget_height ) - bottom = max(bottom, dock_region.height) + bottom = max(bottom, widget_height) elif edge == "top": dock_region = Region(0, 0, widget_width, widget_height) - top = max(top, dock_region.height) + top = max(top, widget_height) elif edge == "left": dock_region = Region(0, 0, widget_width, widget_height) - left = max(left, dock_region.width) + left = max(left, widget_width) elif edge == "right": dock_region = Region( width - widget_width, 0, widget_width, widget_height ) - right = max(right, dock_region.width) + right = max(right, widget_width) else: # Should not occur, mainly to keep Mypy happy raise AssertionError("invalid value for edge") # pragma: no-cover @@ -100,14 +101,17 @@ def arrange( layout_placements, arranged_layout_widgets = widget.layout.arrange( widget, layout_widgets, region.size ) + if arranged_layout_widgets: scroll_spacing = scroll_spacing.grow_maximum(dock_spacing) arrange_widgets.update(arranged_layout_widgets) placement_offset = region.offset if placement_offset: layout_placements = [ - _WidgetPlacement(_region + placement_offset, widget, order, fixed) - for _region, widget, order, fixed in layout_placements + _WidgetPlacement( + _region + placement_offset, layout_widget, order, fixed + ) + for _region, layout_widget, order, fixed in layout_placements ] placements.extend(layout_placements) diff --git a/src/textual/dom.py b/src/textual/dom.py index 68a14b28c..19e9d00f2 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -625,7 +625,7 @@ class DOMNode(MessagePump): query_selector = selector else: query_selector = selector.__name__ - query = DOMQuery(self.screen, filter=query_selector) + query = DOMQuery(self, filter=query_selector) if expect_type is None: return query.first() diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 176586c99..9a0da73fa 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -629,8 +629,8 @@ class Region(NamedTuple): return Region( x=x + left, y=y + top, - width=max(0, width - left - right), - height=max(0, height - top - bottom), + width=max(0, width - (left + right)), + height=max(0, height - (top + bottom)), ) @lru_cache(maxsize=4096) diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index 79ca32809..6d2b5cb74 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -46,9 +46,9 @@ class HorizontalLayout(Layout): x = Fraction(box_models[0].margin.left if box_models else 0) - displayed_children = cast("list[Widget]", parent.displayed_children) + displayed_children = [child for child in children if child.display] - for widget, box_model, margin in zip(displayed_children, box_models, margins): + for widget, box_model, margin in zip(children, box_models, margins): content_width, content_height, box_margin = box_model offset_y = ( widget.styles.align_height( diff --git a/src/textual/widget.py b/src/textual/widget.py index 92ef7018b..ccaa4d2b1 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -210,7 +210,7 @@ class Widget(DOMNode): """ self.app._register(self, *anon_widgets, **widgets) - self.screen.refresh() + self.screen.refresh(layout=True) def compose(self) -> ComposeResult: """Yield child widgets for a container."""