From e354ed142aaaefe74f065bd7abd84fd955f03887 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 1 Sep 2022 16:09:41 +0100 Subject: [PATCH 1/8] Add easing example app, restructure dirs a little --- src/textual/cli/cli.py | 10 ++++++++- src/textual/cli/previews/__init__.py | 0 .../{devtools => cli/previews}/borders.py | 0 src/textual/cli/previews/easing.css | 8 +++++++ src/textual/cli/previews/easing.py | 22 +++++++++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/textual/cli/previews/__init__.py rename src/textual/{devtools => cli/previews}/borders.py (100%) create mode 100644 src/textual/cli/previews/easing.css create mode 100644 src/textual/cli/previews/easing.py diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index 9626d005a..e19f2497d 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -172,6 +172,14 @@ def run_app(import_name: str, dev: bool, press: str) -> None: @run.command("borders") def borders(): """Explore the border styles available in Textual.""" - from ..devtools import borders + from textual.cli.previews import borders borders.app.run() + + +@run.command("easing") +def easing(): + """Explore the animation easing functions available in Textual.""" + from textual.cli.previews import easing + + easing.app.run() diff --git a/src/textual/cli/previews/__init__.py b/src/textual/cli/previews/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/textual/devtools/borders.py b/src/textual/cli/previews/borders.py similarity index 100% rename from src/textual/devtools/borders.py rename to src/textual/cli/previews/borders.py diff --git a/src/textual/cli/previews/easing.css b/src/textual/cli/previews/easing.css new file mode 100644 index 000000000..9b8436a42 --- /dev/null +++ b/src/textual/cli/previews/easing.css @@ -0,0 +1,8 @@ +EasingButtons > Button { + width: 100%; +} +EasingButtons { + dock: left; + width: 20; + overflow: auto auto; +} diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py new file mode 100644 index 000000000..2699b906d --- /dev/null +++ b/src/textual/cli/previews/easing.py @@ -0,0 +1,22 @@ +from textual._easing import EASING +from textual.app import ComposeResult, App +from textual.widget import Widget +from textual.widgets import Button, Static + + +class EasingButtons(Widget): + def compose(self) -> ComposeResult: + for easing in EASING: + yield Button(easing) + + +class EasingApp(App): + def compose(self) -> ComposeResult: + yield EasingButtons() + self.text = Static("Easing examples") + yield self.text + + +app = EasingApp(css_path="easing.css", watch_css=True) +if __name__ == "__main__": + app.run() From b6e133c1af269c0d44da7c3fc2971a915feca37d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 12:12:17 +0100 Subject: [PATCH 2/8] Cleanup easing example --- sandbox/darren/just_a_box.py | 6 +- src/textual/cli/previews/easing.css | 34 +++++++++- src/textual/cli/previews/easing.py | 100 ++++++++++++++++++++++++++-- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/sandbox/darren/just_a_box.py b/sandbox/darren/just_a_box.py index 26b45bb01..8ead1933c 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -5,10 +5,14 @@ from textual.widgets import Static class JustABox(App): + + css_path = "../darren/just_a_box.css" + def compose(self) -> ComposeResult: yield Static("Hello, world!", classes="box1") +app = JustABox(watch_css=True) + if __name__ == "__main__": - app = JustABox(css_path="../darren/just_a_box.css", watch_css=True) app.run() diff --git a/src/textual/cli/previews/easing.css b/src/textual/cli/previews/easing.css index 9b8436a42..a881ece4d 100644 --- a/src/textual/cli/previews/easing.css +++ b/src/textual/cli/previews/easing.css @@ -3,6 +3,38 @@ EasingButtons > Button { } EasingButtons { dock: left; - width: 20; overflow: auto auto; + width: 20; +} + +#bar-container { + content-align: center middle; +} + +#duration-input { + width: 30; +} + +#inputs { + padding: 1; + height: auto; + dock: top; +} + +Bar { + width: 1fr; +} + +#other { + width: 1fr; + background: #555555; + padding: 1; + height: 100%; + border-left: vkey $background; +} + +#opacity-widget { + padding: 1; + background: $panel; + border: wide #969696; } diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index 2699b906d..a9fcff167 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -1,20 +1,110 @@ +from __future__ import annotations + +from rich.console import RenderableType + +from textual import layout from textual._easing import EASING from textual.app import ComposeResult, App +from textual.cli.previews.borders import TEXT +from textual.reactive import Reactive +from textual.scrollbar import ScrollBarRender from textual.widget import Widget -from textual.widgets import Button, Static +from textual.widgets import Button, Static, Footer +from textual.widgets.text_input import TextWidgetBase, TextInput + +VIRTUAL_SIZE = 100 +WINDOW_SIZE = 10 +START_POSITION = 0.0 +END_POSITION = float(VIRTUAL_SIZE - WINDOW_SIZE) class EasingButtons(Widget): def compose(self) -> ComposeResult: - for easing in EASING: - yield Button(easing) + for easing in sorted([e for e in EASING if e != "round"], reverse=True): + yield Button(easing, id=easing) + + +class Bar(Widget): + position = Reactive.init(START_POSITION) + animation_running = Reactive(False) + + def render(self) -> RenderableType: + return ScrollBarRender( + virtual_size=VIRTUAL_SIZE, + window_size=WINDOW_SIZE, + position=self.position, + style="green" if self.animation_running else "red", + ) class EasingApp(App): + position = Reactive.init(START_POSITION) + duration = Reactive.var(1.0) + + def on_load(self): + self.bind( + "ctrl+p", "focus('duration-input')", description="Focus: Duration Input" + ) + self.bind("ctrl+b", "toggle_dark", description="Toggle Dark") + def compose(self) -> ComposeResult: + self.animated_bar = Bar() + self.animated_bar.position = START_POSITION + duration_input = TextInput( + placeholder="Duration", initial="1.0", id="duration-input" + ) + + self.opacity_widget = Static( + f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget" + ) + yield EasingButtons() - self.text = Static("Easing examples") - yield self.text + yield layout.Vertical( + layout.Vertical(Static("Animation Duration:"), duration_input, id="inputs"), + layout.Horizontal( + self.animated_bar, + layout.Container(self.opacity_widget, id="other"), + ), + Footer(), + ) + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.animated_bar.animation_running = True + + def _animation_complete(): + self.animated_bar.animation_running = False + + target_position = ( + END_POSITION if self.position == START_POSITION else START_POSITION + ) + self.animate( + "position", + value=target_position, + final_value=target_position, + duration=self.duration, + easing=event.button.id, + on_complete=_animation_complete, + ) + + def watch_position(self, value: int): + self.animated_bar.position = value + self.opacity_widget.styles.opacity = 1 - value / END_POSITION + + def on_text_widget_base_changed(self, event: TextWidgetBase.Changed): + if event.sender.id == "duration-input": + new_duration = _try_float(event.value) + if new_duration is not None: + self.duration = new_duration + + def action_toggle_dark(self): + self.dark = not self.dark + + +def _try_float(string: str) -> float | None: + try: + return float(string) + except ValueError: + return None app = EasingApp(css_path="easing.css", watch_css=True) From 76e0bdb0c950212f82c97dd472c505e3b4511a56 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 12:13:32 +0100 Subject: [PATCH 3/8] Revert sandbox --- sandbox/darren/just_a_box.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sandbox/darren/just_a_box.py b/sandbox/darren/just_a_box.py index 8ead1933c..52f87ffc4 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -5,14 +5,11 @@ from textual.widgets import Static class JustABox(App): - - css_path = "../darren/just_a_box.css" - def compose(self) -> ComposeResult: yield Static("Hello, world!", classes="box1") -app = JustABox(watch_css=True) +app = JustABox(watch_css=True, css_path="../darren/just_a_box.css") if __name__ == "__main__": app.run() From e233f4bfbc6d6ec2d0c5eefb0a8519cb7d731a58 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 12:19:24 +0100 Subject: [PATCH 4/8] Dont watch_css in easing preview --- src/textual/cli/previews/easing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index a9fcff167..fd2c9ee6a 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -107,6 +107,6 @@ def _try_float(string: str) -> float | None: return None -app = EasingApp(css_path="easing.css", watch_css=True) +app = EasingApp(css_path="easing.css") if __name__ == "__main__": app.run() From c2dd521fbe3334859909df99776f3ac61cf558cb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 12:26:35 +0100 Subject: [PATCH 5/8] Update imports in easing example --- src/textual/cli/previews/easing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index fd2c9ee6a..3e1d39aea 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -10,7 +10,8 @@ from textual.reactive import Reactive from textual.scrollbar import ScrollBarRender from textual.widget import Widget from textual.widgets import Button, Static, Footer -from textual.widgets.text_input import TextWidgetBase, TextInput +from textual.widgets import TextInput +from textual.widgets._text_input import TextWidgetBase VIRTUAL_SIZE = 100 WINDOW_SIZE = 10 From 73d332c38d2561dce40952dc7ae0431eac6dd907 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 13:20:14 +0100 Subject: [PATCH 6/8] Fix animations going negative via scrollbar --- src/textual/scrollbar.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 156954323..afa07f7ee 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -10,7 +10,7 @@ from rich.style import Style, StyleType from . import events from ._types import MessageTarget -from .geometry import Offset +from .geometry import Offset, clamp from .message import Message from .reactive import Reactive from .renderables.blank import Blank @@ -100,6 +100,8 @@ class ScrollBarRender: back = back_color bar = bar_color + len_bars = len(bars) + width_thickness = thickness if vertical else 1 _Segment = Segment @@ -110,11 +112,11 @@ class ScrollBarRender: if window_size and size and virtual_size and size != virtual_size: step_size = virtual_size / size - start = int(position / step_size * 8) - end = start + max(8, int(ceil(window_size / step_size * 8))) + start = int(position / step_size * len_bars) + end = start + max(len_bars, int(ceil(window_size / step_size * len_bars))) - start_index, start_bar = divmod(start, 8) - end_index, end_bar = divmod(end, 8) + start_index, start_bar = divmod(max(0, start), len_bars) + end_index, end_bar = divmod(max(0, end), len_bars) upper = {"@click": "scroll_up"} lower = {"@click": "scroll_down"} @@ -129,8 +131,9 @@ class ScrollBarRender: _Segment(blank, _Style(bgcolor=bar, meta=foreground_meta)) ] * (end_index - start_index) + # Apply the smaller bar characters to head and tail of scrollbar for more "granularity" if start_index < len(segments): - bar_character = bars[7 - start_bar] + bar_character = bars[len_bars - 1 - start_bar] if bar_character != " ": segments[start_index] = _Segment( bar_character * width_thickness, @@ -139,7 +142,7 @@ class ScrollBarRender: else _Style(bgcolor=bar, color=back, meta=foreground_meta), ) if end_index < len(segments): - bar_character = bars[7 - end_bar] + bar_character = bars[len_bars - 1 - end_bar] if bar_character != " ": segments[end_index] = _Segment( bar_character * width_thickness, From 5018de11a664ce523a5c1eacc1763a7fb7094cec Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 15:39:53 +0100 Subject: [PATCH 7/8] Remove unused import --- src/textual/scrollbar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index afa07f7ee..c223ca73b 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -10,7 +10,7 @@ from rich.style import Style, StyleType from . import events from ._types import MessageTarget -from .geometry import Offset, clamp +from .geometry import Offset from .message import Message from .reactive import Reactive from .renderables.blank import Blank From 39a45bc464e94336f19071b8328b297a8846e866 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 7 Sep 2022 15:42:04 +0100 Subject: [PATCH 8/8] Re-add round easing --- src/textual/cli/previews/easing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index 3e1d39aea..b4cb08e35 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -21,7 +21,7 @@ END_POSITION = float(VIRTUAL_SIZE - WINDOW_SIZE) class EasingButtons(Widget): def compose(self) -> ComposeResult: - for easing in sorted([e for e in EASING if e != "round"], reverse=True): + for easing in sorted(EASING, reverse=True): yield Button(easing, id=easing)