From dcfbbc38b6bc29e239fda397a0db64f16c07f551 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 13:00:40 +0000 Subject: [PATCH 01/31] demo updates --- CHANGELOG.md | 1 + src/textual/_xterm_parser.py | 1 + src/textual/app.py | 4 +- src/textual/demo/demo_app.py | 34 +++++++++++++ src/textual/demo/home.py | 2 +- src/textual/demo/widgets.py | 61 +++++++++++++++++++++++- src/textual/drivers/linux_driver.py | 2 - src/textual/events.py | 16 ++++++- src/textual/message_pump.py | 2 + src/textual/renderables/_blend_colors.py | 6 ++- src/textual/screen.py | 9 +++- src/textual/widgets/_list_view.py | 2 + src/textual/widgets/_radio_set.py | 2 + src/textual/widgets/_switch.py | 1 + 14 files changed, 133 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76279be25a..f61dceb404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `can_focus` and `can_focus_children` parameters to scrollable container types. https://github.com/Textualize/textual/pull/5226 - Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226 - Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226 +- `Click` events can now be used with the on decorator to match the initially clicked widget ### Changed diff --git a/src/textual/_xterm_parser.py b/src/textual/_xterm_parser.py index ddb935f505..6382413c77 100644 --- a/src/textual/_xterm_parser.py +++ b/src/textual/_xterm_parser.py @@ -93,6 +93,7 @@ def parse_mouse_code(self, code: str) -> Message | None: event_class = events.MouseDown if state == "M" else events.MouseUp event = event_class( + None, x, y, delta_x, diff --git a/src/textual/app.py b/src/textual/app.py index d4207d4400..4503872100 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -3702,7 +3702,9 @@ async def on_event(self, event: events.Event) -> None: self.get_widget_at(event.x, event.y)[0] is self._mouse_down_widget ): - click_event = events.Click.from_event(event) + click_event = events.Click.from_event( + self._mouse_down_widget, event + ) self.screen._forward_event(click_event) except NoWidget: pass diff --git a/src/textual/demo/demo_app.py b/src/textual/demo/demo_app.py index 0121f031b8..a3ec87bf94 100644 --- a/src/textual/demo/demo_app.py +++ b/src/textual/demo/demo_app.py @@ -15,6 +15,14 @@ class DemoApp(App): align: center top; &>*{ max-width: 100; } } + Screen .-maximized { + margin: 1 2; + max-width: 100%; + &.column { margin: 1 2; padding: 1 2; } + &.column > * { + max-width: 100%; + } + } """ MODES = { @@ -48,4 +56,30 @@ class DemoApp(App): "Screenshot", tooltip="Save an SVG 'screenshot' of the current screen", ), + Binding( + "ctrl+m", + "app.maximize", + "Maximize", + tooltip="Maximized the focused widget (if possible)", + ), ] + + def action_maximize(self) -> None: + if self.screen.focused is None: + self.notify( + "Nothing to be maximized (try pressing [b]tab[/b])", + title="Maximize", + severity="warning", + ) + else: + if self.screen.maximize(self.screen.focused): + self.notify( + "You are now in the maximized view. Press [b]escape[/b] to return.", + title="Maximize", + ) + else: + self.notify( + "This widget may not be maximized.", + title="Maximize", + severity="warning", + ) diff --git a/src/textual/demo/home.py b/src/textual/demo/home.py index df78333dd4..2b9c6d663b 100644 --- a/src/textual/demo/home.py +++ b/src/textual/demo/home.py @@ -145,7 +145,7 @@ class StarCount(Vertical): color: $text-warning; #stars { align: center top; } #forks { align: right top; } - Label { text-style: bold; } + Label { text-style: bold; color: $foreground; } LoadingIndicator { background: transparent !important; } Digits { width: auto; margin-right: 1; } Label { margin-right: 1; } diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index dd36cb1f8c..ddf44983cc 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -8,13 +8,14 @@ from rich.table import Table from rich.traceback import Traceback -from textual import containers, lazy +from textual import containers, events, lazy, on from textual.app import ComposeResult from textual.binding import Binding from textual.demo.data import COUNTRIES from textual.demo.page import PageScreen from textual.reactive import reactive, var from textual.suggester import SuggestFromList +from textual.theme import BUILTIN_THEMES from textual.widgets import ( Button, Checkbox, @@ -33,6 +34,7 @@ RadioSet, RichLog, Sparkline, + Switch, TabbedContent, ) @@ -49,6 +51,7 @@ class Buttons(containers.VerticalGroup): """Buttons demo.""" + ALLOW_MAXIMIZE = True DEFAULT_CLASSES = "column" DEFAULT_CSS = """ Buttons { @@ -179,6 +182,7 @@ def on_mount(self) -> None: class Inputs(containers.VerticalGroup): """Demonstrates Inputs.""" + ALLOW_MAXIMIZE = True DEFAULT_CLASSES = "column" INPUTS_MD = """\ ## Inputs and MaskedInputs @@ -234,6 +238,7 @@ def compose(self) -> ComposeResult: class ListViews(containers.VerticalGroup): """Demonstrates List Views and Option Lists.""" + ALLOW_MAXIMIZE = True DEFAULT_CLASSES = "column" LISTS_MD = """\ ## List Views and Option Lists @@ -435,6 +440,59 @@ def update_sparks(self) -> None: self.data = [abs(sin(x / 3.14)) for x in range(offset, offset + 360 * 6, 20)] +class Switches(containers.VerticalGroup): + """Demonstrate the Switch widget.""" + + ALLOW_MAXIMIZE = True + DEFAULT_CLASSES = "column" + SWITCHES_MD = """\ +## Switches + +Functionally almost identical to a Checkbox, but more displays more prominently in the UI. +""" + DEFAULT_CSS = """\ +Switches { + Label { + padding: 1; + &:hover {text-style:underline; } + } +} +""" + + def compose(self) -> ComposeResult: + yield Markdown(self.SWITCHES_MD) + with containers.ItemGrid(min_column_width=32): + for theme in BUILTIN_THEMES: + with containers.HorizontalGroup(): + yield Switch(id=theme) + yield Label(theme, name=theme) + + @on(events.Click, "Label") + def on_click(self, event: events.Click) -> None: + """Make the label toggle the switch.""" + # TODO: Add a dedicated form label + event.stop() + if event.widget is not None: + self.query_one(f"#{event.widget.name}", Switch).toggle() + + def on_switch_changed(self, event: Switch.Changed) -> None: + # Don't issue more Changed events + with self.prevent(Switch.Changed): + # Reset all other switches + for switch in self.query("Switch").results(Switch): + if switch.id != event.switch.id: + switch.value = False + assert event.switch.id is not None + theme_id = event.switch.id + + def switch_theme() -> None: + """Callback to switch the theme.""" + self.app.theme = theme_id + + # Call after a short delay, so we see the Switch animation + self.set_timer(0.3, switch_theme) + + class WidgetsScreen(PageScreen): """The Widgets screen""" @@ -467,4 +525,5 @@ def compose(self) -> ComposeResult: yield ListViews() yield Logs() yield Sparklines() + yield Switches() yield Footer() diff --git a/src/textual/drivers/linux_driver.py b/src/textual/drivers/linux_driver.py index 0bfd5cbf1d..510b0992f0 100644 --- a/src/textual/drivers/linux_driver.py +++ b/src/textual/drivers/linux_driver.py @@ -267,8 +267,6 @@ def on_terminal_resize(signum, stack) -> None: self.write("\x1b[?25l") # Hide cursor self.write("\x1b[?1004h") # Enable FocusIn/FocusOut. self.write("\x1b[>1u") # https://sw.kovidgoyal.net/kitty/keyboard-protocol/ - # Disambiguate escape codes https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement - self.write("\x1b[=1;u") self.flush() self._key_thread = Thread(target=self._run_input_thread) diff --git a/src/textual/events.py b/src/textual/events.py index e8fa1fa9e8..6e7a21e8da 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -327,6 +327,7 @@ class MouseEvent(InputEvent, bubble=True): - [ ] Verbose Args: + widget: The widget under the mouse. x: The relative x coordinate. y: The relative y coordinate. delta_x: Change in x since the last message. @@ -341,6 +342,7 @@ class MouseEvent(InputEvent, bubble=True): """ __slots__ = [ + "widget", "x", "y", "delta_x", @@ -356,6 +358,7 @@ class MouseEvent(InputEvent, bubble=True): def __init__( self, + widget: Widget | None, x: int, y: int, delta_x: int, @@ -369,6 +372,8 @@ def __init__( style: Style | None = None, ) -> None: super().__init__() + self.widget: Widget | None = widget + """The widget under the mouse at the time of a click.""" self.x = x """The relative x coordinate.""" self.y = y @@ -392,8 +397,11 @@ def __init__( self._style = style or Style() @classmethod - def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT: + def from_event( + cls: Type[MouseEventT], widget: Widget, event: MouseEvent + ) -> MouseEventT: new_event = cls( + widget, event.x, event.y, event.delta_x, @@ -409,6 +417,7 @@ def from_event(cls: Type[MouseEventT], event: MouseEvent) -> MouseEventT: return new_event def __rich_repr__(self) -> rich.repr.Result: + yield self.widget yield "x", self.x yield "y", self.y yield "delta_x", self.delta_x, 0 @@ -422,6 +431,10 @@ def __rich_repr__(self) -> rich.repr.Result: yield "meta", self.meta, False yield "ctrl", self.ctrl, False + @property + def control(self) -> Widget | None: + return self.widget + @property def offset(self) -> Offset: """The mouse coordinate as an offset. @@ -478,6 +491,7 @@ def get_content_offset_capture(self, widget: Widget) -> Offset: def _apply_offset(self, x: int, y: int) -> MouseEvent: return self.__class__( + self.widget, x=self.x + x, y=self.y + y, delta_x=self.delta_x, diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 6ac31ac8eb..58fb76d1c1 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -725,6 +725,8 @@ def _get_dispatch_methods( continue for attribute, selector in selectors.items(): node = getattr(message, attribute) + if node is None: + break if not isinstance(node, Widget): raise OnNoWidget( f"on decorator can't match against {attribute!r} as it is not a widget." diff --git a/src/textual/renderables/_blend_colors.py b/src/textual/renderables/_blend_colors.py index 305675103a..c08f4ddc45 100644 --- a/src/textual/renderables/_blend_colors.py +++ b/src/textual/renderables/_blend_colors.py @@ -15,8 +15,10 @@ def blend_colors(color1: Color, color2: Color, ratio: float) -> Color: Returns: A Color representing the blending of the two supplied colors. """ - assert color1.triplet is not None - assert color2.triplet is not None + # assert color1.triplet is not None + # assert color2.triplet is not None + if color1.triplet is None or color2.triplet is None: + return color2 r1, g1, b1 = color1.triplet r2, g2, b2 = color2.triplet diff --git a/src/textual/screen.py b/src/textual/screen.py index 9bea8be709..8b22c857a8 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -746,12 +746,15 @@ def focus_previous(self, selector: str | type[QueryType] = "*") -> Widget | None """ return self._move_focus(-1, selector) - def maximize(self, widget: Widget, container: bool = True) -> None: + def maximize(self, widget: Widget, container: bool = True) -> bool: """Maximize a widget, so it fills the screen. Args: widget: Widget to maximize. container: If one of the widgets ancestors is a maximizeable widget, maximize that instead. + + Returns: + `True` if the widget was maximized, otherwise `False`. """ if widget.allow_maximize: if container: @@ -761,9 +764,10 @@ def maximize(self, widget: Widget, container: bool = True) -> None: break if maximize_widget.allow_maximize: self.maximized = maximize_widget - return + return True self.maximized = widget + return False def minimize(self) -> None: """Restore any maximized widget to normal state.""" @@ -1371,6 +1375,7 @@ def _translate_mouse_move_event( the origin of the specified region. """ return events.MouseMove( + event.widget, event.x - region.x, event.y - region.y, event.delta_x, diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index bc81a0936f..dea43c291f 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -25,6 +25,8 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False): index: The index in the list that's currently highlighted. """ + ALLOW_MAXIMIZE = True + DEFAULT_CSS = """ ListView { background: $surface; diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 61802999be..befb4372df 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -25,6 +25,8 @@ class RadioSet(VerticalScroll, can_focus=True, can_focus_children=False): turned off. """ + ALLOW_MAXIMIZE = True + DEFAULT_CSS = """ RadioSet { border: tall $border-blurred; diff --git a/src/textual/widgets/_switch.py b/src/textual/widgets/_switch.py index 37714a62ea..7d57b28b48 100644 --- a/src/textual/widgets/_switch.py +++ b/src/textual/widgets/_switch.py @@ -50,6 +50,7 @@ class Switch(Widget, can_focus=True): background: $surface; height: auto; width: auto; + padding: 0 2; &.-on > .switch--slider { color: $success; From f84b166cdf7308af9ae89fabfa4acad6eb62a51d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 13:45:59 +0000 Subject: [PATCH 02/31] copy tweak --- src/textual/demo/data.py | 2 ++ src/textual/demo/home.py | 15 ++++++++++++--- src/textual/screen.py | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/textual/demo/data.py b/src/textual/demo/data.py index 6023ec83e2..1d47f1894d 100644 --- a/src/textual/demo/data.py +++ b/src/textual/demo/data.py @@ -196,3 +196,5 @@ "Zambia", "Zimbabwe", ] +# Sort by length for auto-complete +COUNTRIES.sort(key=str.__len__) diff --git a/src/textual/demo/home.py b/src/textual/demo/home.py index 2b9c6d663b..00b23ec574 100644 --- a/src/textual/demo/home.py +++ b/src/textual/demo/home.py @@ -66,11 +66,20 @@ ```python # Start building! -import textual +from textual import App, ComposeResult +from textual.widgets import Label + +class MyApp(App): + def compose(self) -> ComposeResult: + yield Label("Hello, World!") + +MyApp().run() ``` -Well documented, typed, and intuitive. -Textual's API is accessible to Python developers of all skill levels. +* Well documented: See the [tutorial](https://textual.textualize.io/tutorial/), [guide](https://textual.textualize.io/guide/app/), and [reference](https://textual.textualize.io/reference/). +* Fully typed, with modern type annotations. +* Intuitive, batteries-included, API +* Accessible to Python developers of all skill levels. **Hint:** press **C** to view the code for this page. diff --git a/src/textual/screen.py b/src/textual/screen.py index 8b22c857a8..81cc3c733a 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -767,6 +767,7 @@ def maximize(self, widget: Widget, container: bool = True) -> bool: return True self.maximized = widget + return True return False def minimize(self) -> None: From e377822a820b3dff5fa8de22c637cbe2a28988eb Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 14:03:41 +0000 Subject: [PATCH 03/31] copy --- src/textual/demo/home.py | 6 ++++-- src/textual/widgets/_tabbed_content.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/textual/demo/home.py b/src/textual/demo/home.py index 00b23ec574..24c770257f 100644 --- a/src/textual/demo/home.py +++ b/src/textual/demo/home.py @@ -85,8 +85,8 @@ def compose(self) -> ComposeResult: ## Built on Rich -With over 1.5 *billion* downloads, Rich is the most popular terminal library out there. -Textual builds on Rich to add interactivity, and is compatible with Rich renderables. +With over 1.6 *billion* downloads, Rich is the most popular terminal library out there. +Textual builds on Rich to add interactivity, and is fully-compatible with Rich renderables. ## Re-usable widgets @@ -122,6 +122,8 @@ def compose(self) -> ComposeResult: """ DEPLOY_MD = """\ +Textual apps have extremely low system requirements, and will run on virtually any OS and hardware; locally or remotely via SSH. + There are a number of ways to deploy and share Textual apps. ## As a Python library diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index 2355c82275..37630b05dc 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -236,6 +236,7 @@ def _on_descendant_focus(self, event: events.DescendantFocus): class TabbedContent(Widget): """A container with associated tabs to toggle content visibility.""" + ALLOW_MAXIMIZE = True DEFAULT_CSS = """ TabbedContent { height: auto; From ece2e8900671da780e8b18ef42f3a9f6b462eede Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 15:51:33 +0000 Subject: [PATCH 04/31] more data in data table --- src/textual/command.py | 2 +- src/textual/demo/data.py | 127 ++++++++++++++++++++++++++++++++++++ src/textual/demo/home.py | 2 +- src/textual/demo/widgets.py | 32 ++++----- 4 files changed, 145 insertions(+), 18 deletions(-) diff --git a/src/textual/command.py b/src/textual/command.py index 3bb575af4c..b6174af86a 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -555,7 +555,7 @@ class CommandPalette(SystemModalScreen[None]): CommandPalette > .command-palette--help-text { color: $text-muted; background: transparent; - text-style: not bold dim; + text-style: not bold; } CommandPalette > .command-palette--highlight { diff --git a/src/textual/demo/data.py b/src/textual/demo/data.py index 1d47f1894d..19b2f7dde1 100644 --- a/src/textual/demo/data.py +++ b/src/textual/demo/data.py @@ -198,3 +198,130 @@ ] # Sort by length for auto-complete COUNTRIES.sort(key=str.__len__) + +# Thanks, Claude +MOVIES = """\ +Date,Title,Genre,Director,Box Office (millions),Rating,Runtime (min) +1980-01-18,The Fog,Horror,John Carpenter,21,R,89 +1980-02-15,Coal Miner's Daughter,Biography,Michael Apted,67,PG,124 +1980-03-07,Little Miss Marker,Comedy,Walter Bernstein,12,PG,103 +1980-04-11,The Long Riders,Western,Walter Hill,15,R,100 +1980-05-21,The Empire Strikes Back,Sci-Fi,Irvin Kershner,538,PG,124 +1980-06-13,The Blues Brothers,Comedy,John Landis,115,R,133 +1980-07-02,Airplane!,Comedy,Jim Abrahams,83,PG,88 +1980-08-01,Caddyshack,Comedy,Harold Ramis,39,R,98 +1980-09-19,The Big Red One,War,Samuel Fuller,24,PG,113 +1980-10-10,Private Benjamin,Comedy,Howard Zieff,69,R,109 +1980-11-07,The Stunt Man,Action,Richard Rush,7,R,131 +1980-12-19,Nine to Five,Comedy,Colin Higgins,103,PG,109 +1981-01-23,Scanners,Horror,David Cronenberg,14,R,103 +1981-02-20,The Final Conflict,Horror,Graham Baker,20,R,108 +1981-03-20,Raiders of the Lost Ark,Action,Steven Spielberg,389,PG,115 +1981-04-10,Excalibur,Fantasy,John Boorman,35,R,140 +1981-05-22,Outland,Sci-Fi,Peter Hyams,17,R,109 +1981-06-19,Superman II,Action,Richard Lester,108,PG,127 +1981-07-17,Escape from New York,Sci-Fi,John Carpenter,25,R,99 +1981-08-07,An American Werewolf in London,Horror,John Landis,30,R,97 +1981-09-25,Continental Divide,Romance,Michael Apted,15,PG,103 +1981-10-16,True Confessions,Drama,Ulu Grosbard,12,R,108 +1981-11-20,Time Bandits,Fantasy,Terry Gilliam,42,PG,116 +1981-12-04,Rollover,Drama,Alan J. Pakula,11,R,116 +1982-01-15,The Beast Within,Horror,Philippe Mora,7,R,98 +1982-02-12,Quest for Fire,Adventure,Jean-Jacques Annaud,20,R,100 +1982-03-19,Porky's,Comedy,Bob Clark,105,R,94 +1982-04-16,The Sword and the Sorcerer,Fantasy,Albert Pyun,39,R,99 +1982-05-14,Conan the Barbarian,Fantasy,John Milius,68,R,129 +1982-06-04,Star Trek II: The Wrath of Khan,Sci-Fi,Nicholas Meyer,97,PG,113 +1982-06-11,E.T. the Extra-Terrestrial,Sci-Fi,Steven Spielberg,792,PG,115 +1982-06-25,Blade Runner,Sci-Fi,Ridley Scott,33,R,117 +1982-07-16,The World According to Garp,Comedy-Drama,George Roy Hill,29,R,136 +1982-08-13,Fast Times at Ridgemont High,Comedy,Amy Heckerling,27,R,90 +1982-09-17,The Challenge,Action,John Frankenheimer,9,R,108 +1982-10-22,First Blood,Action,Ted Kotcheff,47,R,93 +1982-11-12,The Man from Snowy River,Western,George Miller,20,PG,102 +1982-12-08,48 Hrs.,Action,Walter Hill,79,R,96 +1983-01-21,The Entity,Horror,Sidney J. Furie,13,R,125 +1983-02-18,The Year of Living Dangerously,Drama,Peter Weir,10,PG,115 +1983-03-25,The Outsiders,Drama,Francis Ford Coppola,25,PG,91 +1983-04-22,Something Wicked This Way Comes,Horror,Jack Clayton,5,PG,95 +1983-05-25,Return of the Jedi,Sci-Fi,Richard Marquand,475,PG,131 +1983-06-17,Superman III,Action,Richard Lester,60,PG,125 +1983-07-15,Class,Comedy,Lewis John Carlino,21,R,98 +1983-08-19,Curse of the Pink Panther,Comedy,Blake Edwards,9,PG,109 +1983-09-23,The Big Chill,Drama,Lawrence Kasdan,56,R,105 +1983-10-07,The Right Stuff,Drama,Philip Kaufman,21,PG,193 +1983-11-04,Deal of the Century,Comedy,William Friedkin,10,PG,99 +1983-12-09,Scarface,Crime,Brian De Palma,65,R,170 +1984-01-13,Terms of Endearment,Drama,James L. Brooks,108,PG,132 +1984-02-17,Unfaithfully Yours,Comedy,Howard Zieff,12,PG,96 +1984-03-16,Splash,Romance,Ron Howard,69,PG,111 +1984-04-13,Friday the 13th: The Final Chapter,Horror,Joseph Zito,32,R,91 +1984-05-04,Sixteen Candles,Comedy,John Hughes,23,PG,93 +1984-06-08,Ghostbusters,Comedy,Ivan Reitman,295,PG,105 +1984-07-06,The Last Starfighter,Sci-Fi,Nick Castle,28,PG,101 +1984-08-10,Red Dawn,Action,John Milius,38,PG-13,114 +1984-09-14,All of Me,Comedy,Carl Reiner,40,PG,93 +1984-10-26,The Terminator,Sci-Fi,James Cameron,78,R,107 +1984-11-16,Missing in Action,Action,Joseph Zito,22,R,101 +1984-12-14,Dune,Sci-Fi,David Lynch,30,PG-13,137 +1985-01-18,A Nightmare on Elm Street,Horror,Wes Craven,25,R,91 +1985-02-15,The Breakfast Club,Drama,John Hughes,45,R,97 +1985-03-29,Mask,Drama,Peter Bogdanovich,42,PG-13,120 +1985-04-26,Code of Silence,Action,Andrew Davis,20,R,101 +1985-05-22,Rambo: First Blood Part II,Action,George P. Cosmatos,150,R,96 +1985-06-07,The Goonies,Adventure,Richard Donner,61,PG,114 +1985-07-03,Back to the Future,Sci-Fi,Robert Zemeckis,381,PG,116 +1985-08-16,Year of the Dragon,Crime,Michael Cimino,18,R,134 +1985-09-20,Invasion U.S.A.,Action,Joseph Zito,17,R,107 +1985-10-18,Silver Bullet,Horror,Daniel Attias,12,R,95 +1985-11-22,Rocky IV,Drama,Sylvester Stallone,127,PG,91 +1985-12-20,The Color Purple,Drama,Steven Spielberg,142,PG-13,154 +1986-01-17,Iron Eagle,Action,Sidney J. Furie,24,PG-13,117 +1986-02-21,Crossroads,Drama,Walter Hill,5,R,99 +1986-03-21,Highlander,Fantasy,Russell Mulcahy,12,R,116 +1986-04-18,Legend,Fantasy,Ridley Scott,15,PG,89 +1986-05-16,Top Gun,Action,Tony Scott,357,PG,110 +1986-06-27,Running Scared,Action,Peter Hyams,38,R,107 +1986-07-18,Aliens,Sci-Fi,James Cameron,131,R,137 +1986-08-08,Stand By Me,Drama,Rob Reiner,52,R,89 +1986-09-19,Blue Velvet,Mystery,David Lynch,8,R,120 +1986-10-24,The Name of the Rose,Mystery,Jean-Jacques Annaud,7,R,130 +1986-11-21,An American Tail,Animation,Don Bluth,47,G,80 +1986-12-19,Star Trek IV: The Voyage Home,Sci-Fi,Leonard Nimoy,109,PG,119 +1987-01-23,Critical Condition,Comedy,Michael Apted,19,R,98 +1987-02-20,Death Before Dishonor,Action,Terry Leonard,3,R,91 +1987-03-13,Lethal Weapon,Action,Richard Donner,65,R,110 +1987-04-10,Project X,Drama,Jonathan Kaplan,28,PG,108 +1987-05-22,Beverly Hills Cop II,Action,Tony Scott,276,R,100 +1987-06-19,Predator,Sci-Fi,John McTiernan,98,R,107 +1987-07-17,RoboCop,Action,Paul Verhoeven,53,R,102 +1987-08-14,No Way Out,Thriller,Roger Donaldson,35,R,114 +1987-09-18,Fatal Beauty,Action,Tom Holland,12,R,104 +1987-10-23,Fatal Attraction,Thriller,Adrian Lyne,320,R,119 +1987-11-13,Running Man,Sci-Fi,Paul Michael Glaser,38,R,101 +1987-12-18,Wall Street,Drama,Oliver Stone,43,R,126 +1988-01-15,Return of the Living Dead Part II,Horror,Ken Wiederhorn,9,R,89 +1988-02-12,Action Jackson,Action,Craig R. Baxley,20,R,96 +1988-03-18,D.O.A.,Thriller,Rocky Morton,12,R,96 +1988-04-29,Colors,Crime,Dennis Hopper,46,R,120 +1988-05-20,Willow,Fantasy,Ron Howard,57,PG,126 +1988-06-21,Big,Comedy,Penny Marshall,151,PG,104 +1988-07-15,Die Hard,Action,John McTiernan,140,R,132 +1988-08-05,Young Guns,Western,Christopher Cain,45,R,107 +1988-09-16,Moon Over Parador,Comedy,Paul Mazursky,11,PG-13,103 +1988-10-21,Halloween 4,Horror,Dwight H. Little,17,R,88 +1988-11-11,Child's Play,Horror,Tom Holland,33,R,87 +1988-12-21,Rain Man,Drama,Barry Levinson,172,R,133 +1989-01-13,Deep Star Six,Sci-Fi,Sean S. Cunningham,8,R,99 +1989-02-17,Bill & Ted's Excellent Adventure,Comedy,Stephen Herek,40,PG,90 +1989-03-24,Leviathan,Sci-Fi,George P. Cosmatos,15,R,98 +1989-04-14,Major League,Comedy,David S. Ward,49,R,107 +1989-05-24,Indiana Jones and the Last Crusade,Action,Steven Spielberg,474,PG-13,127 +1989-06-23,Batman,Action,Tim Burton,411,PG-13,126 +1989-07-07,Lethal Weapon 2,Action,Richard Donner,227,R,114 +1989-08-11,A Nightmare on Elm Street 5,Horror,Stephen Hopkins,22,R,89 +1989-09-22,Black Rain,Action,Ridley Scott,46,R,125 +1989-10-20,Look Who's Talking,Comedy,Amy Heckerling,140,PG-13,93 +1989-11-17,All Dogs Go to Heaven,Animation,Don Bluth,27,G,84 +1989-12-20,Tango & Cash,Action,Andrei Konchalovsky,63,R,104 +""" diff --git a/src/textual/demo/home.py b/src/textual/demo/home.py index 24c770257f..7a8cb836e6 100644 --- a/src/textual/demo/home.py +++ b/src/textual/demo/home.py @@ -76,9 +76,9 @@ def compose(self) -> ComposeResult: MyApp().run() ``` +* Intuitive, batteries-included, API. * Well documented: See the [tutorial](https://textual.textualize.io/tutorial/), [guide](https://textual.textualize.io/guide/app/), and [reference](https://textual.textualize.io/reference/). * Fully typed, with modern type annotations. -* Intuitive, batteries-included, API * Accessible to Python developers of all skill levels. **Hint:** press **C** to view the code for this page. diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index ddf44983cc..da14c0188b 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -11,7 +11,7 @@ from textual import containers, events, lazy, on from textual.app import ComposeResult from textual.binding import Binding -from textual.demo.data import COUNTRIES +from textual.demo.data import COUNTRIES, MOVIES from textual.demo.page import PageScreen from textual.reactive import reactive, var from textual.suggester import SuggestFromList @@ -154,29 +154,29 @@ class Datatables(containers.VerticalGroup): A fully-featured DataTable, with cell, row, and columns cursors. Cells may be individually styled, and may include Rich renderables. +**Tip:** Focus the table and press `ctrl+m` + """ - ROWS = [ - ("lane", "swimmer", "country", "time"), - (4, "Joseph Schooling", "Singapore", 50.39), - (2, "Michael Phelps", "United States", 51.14), - (5, "Chad le Clos", "South Africa", 51.14), - (6, "László Cseh", "Hungary", 51.14), - (3, "Li Zhuhao", "China", 51.26), - (8, "Mehdy Metella", "France", 51.58), - (7, "Tom Shields", "United States", 51.73), - (1, "Aleksandr Sadovnikov", "Russia", 51.84), - (10, "Darren Burns", "Scotland", 51.84), - ] + DEFAULT_CSS = """ + DataTable { + height: 16 !important; + &.-maximized { + height: auto !important; + } + } + + """ def compose(self) -> ComposeResult: yield Markdown(self.DATATABLES_MD) with containers.Center(): - yield DataTable() + yield DataTable(fixed_columns=1) def on_mount(self) -> None: + ROWS = list(csv.reader(io.StringIO(MOVIES))) table = self.query_one(DataTable) - table.add_columns(*self.ROWS[0]) - table.add_rows(self.ROWS[1:]) + table.add_columns(*ROWS[0]) + table.add_rows(ROWS[1:]) class Inputs(containers.VerticalGroup): From caa7dde4f1b095a093a2e40f229555c7b108db84 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 16:53:07 +0000 Subject: [PATCH 05/31] default theme logic --- CHANGELOG.md | 1 + src/textual/app.py | 13 ++++++++++--- src/textual/constants.py | 5 +++++ src/textual/demo/widgets.py | 26 +++++++++++++++++++------- src/textual/driver.py | 1 + 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f61dceb404..dd5f8ca3e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `textual.theme.ThemeProvider`, a command palette provider which returns all registered themes https://github.com/Textualize/textual/pull/5087 - Added several new built-in CSS variables https://github.com/Textualize/textual/pull/5087 - Added support for in-band terminal resize protocol https://github.com/Textualize/textual/pull/5217 +- Added TEXTUAL_THEME environment var, which should be a comma separated list of desired themes ### Changed diff --git a/src/textual/app.py b/src/textual/app.py index 4503872100..92147561ff 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -487,7 +487,7 @@ class MyApp(App[None]): get focus when the terminal widget has focus. """ - theme: Reactive[str] = Reactive("textual-dark") + theme: Reactive[str] = Reactive(constants.DEFAULT_THEME) """The name of the currently active theme.""" ansi_theme_dark = Reactive(MONOKAI, init=False) @@ -1186,12 +1186,17 @@ def get_theme(self, theme_name: str) -> Theme | None: """Get a theme by name. Args: - theme_name: The name of the theme to get. + theme_name: The name of the theme to get. May also be a comma + separated list of names, to pick the first available theme. Returns: A Theme instance and None if the theme doesn't exist. """ - return self.available_themes[theme_name] + theme_names = [token.strip() for token in theme_name.split(",")] + for theme_name in theme_names: + if theme_name in self.available_themes: + return self.available_themes[theme_name] + return None def register_theme(self, theme: Theme) -> None: """Register a theme with the app. @@ -1227,6 +1232,8 @@ def available_themes(self) -> dict[str, Theme]: @property def current_theme(self) -> Theme: theme = self.get_theme(self.theme) + if theme is None: + theme = self.get_theme("textual-dark") assert theme is not None # validated by _validate_theme return theme diff --git a/src/textual/constants.py b/src/textual/constants.py index f4d1673888..7313fa614f 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -152,3 +152,8 @@ def _get_textual_animations() -> AnimationLevel: """The time threshold (in milliseconds) after which a warning is logged if message processing exceeds this duration. """ + +DEFAULT_THEME: Final[str] = get_environ("TEXTUAL_THEME", "") +"""Textual theme to make default. More than one theme may be specified in a comma separated list. +Textual will use the first theme that exists. +""" diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index da14c0188b..c68dd2a8d5 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -66,6 +66,8 @@ class Buttons(containers.VerticalGroup): A simple button, with a number of semantic styles. May be rendered unclickable by setting `disabled=True`. +Press `return` to active a button when focused (or click it). + """ def compose(self) -> ComposeResult: @@ -119,18 +121,28 @@ class Checkboxes(containers.VerticalGroup): Checkboxes to toggle booleans. Radio buttons for exclusive booleans. -Radio sets for a managed set of options where only a single option may be selected. + +Hit `return` to toggle an checkbox / radio button, when focused. """ + RADIOSET_MD = """\ + +### Radio Sets + +A *radio set* is a list of mutually exclusive options. +Use the `up` and `down` keys to navigate the list. +Press `return` to toggle a radio button. + +""" def compose(self) -> ComposeResult: yield Markdown(self.CHECKBOXES_MD) - with containers.HorizontalGroup(): - with containers.VerticalGroup(): - yield Checkbox("Arrakis") - yield Checkbox("Caladan") - yield RadioButton("Chusuk") - yield RadioButton("Giedi Prime") + + with containers.VerticalGroup(): + yield Checkbox("A Checkbox") + yield RadioButton("A Radio Button") + + yield Markdown(self.RADIOSET_MD) yield RadioSet( "Amanda", "Connor MacLeod", diff --git a/src/textual/driver.py b/src/textual/driver.py index b16b982b5e..1fa57c406b 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -113,6 +113,7 @@ def process_message(self, message: messages.Message) -> None: for button in buttons: self.send_message( MouseUp( + message.widget, x=move_event.x, y=move_event.y, delta_x=0, From 0a7a292c5168e757476f5964c88f5292b3fc8cbd Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 17:32:52 +0000 Subject: [PATCH 06/31] maximize demo widgets --- src/textual/containers.py | 9 +++++ src/textual/demo/widgets.py | 75 ++++++++++++++++++++++--------------- src/textual/widget.py | 11 ++++++ 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/textual/containers.py b/src/textual/containers.py index aebd3273c5..18a46d1455 100644 --- a/src/textual/containers.py +++ b/src/textual/containers.py @@ -82,6 +82,7 @@ def __init__( disabled: bool = False, can_focus: bool | None = None, can_focus_children: bool | None = None, + can_maximize: bool | None = None, ) -> None: """ @@ -93,6 +94,7 @@ def __init__( disabled: Whether the widget is disabled or not. can_focus: Can this container be focused? can_focus_children: Can this container's children be focused? + can_maximized: Allow this container to maximize? `None` to use default logic., """ super().__init__( @@ -106,6 +108,13 @@ def __init__( self.can_focus = can_focus if can_focus_children is not None: self.can_focus_children = can_focus_children + self.can_maximize = can_maximize + + @property + def allow_maximize(self) -> bool: + if self.can_maximize is None: + return super().allow_maximize + return self.can_maximize class Vertical(Widget, inherit_bindings=False): diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index c68dd2a8d5..00cce0ea72 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -137,23 +137,20 @@ class Checkboxes(containers.VerticalGroup): def compose(self) -> ComposeResult: yield Markdown(self.CHECKBOXES_MD) - - with containers.VerticalGroup(): - yield Checkbox("A Checkbox") - yield RadioButton("A Radio Button") - - yield Markdown(self.RADIOSET_MD) - yield RadioSet( - "Amanda", - "Connor MacLeod", - "Duncan MacLeod", - "Heather MacLeod", - "Joe Dawson", - "Kurgan, [bold italic red]The[/]", - "Methos", - "Rachel Ellenstein", - "Ramírez", - ) + yield Checkbox("A Checkbox") + yield RadioButton("A Radio Button") + yield Markdown(self.RADIOSET_MD) + yield RadioSet( + "Amanda", + "Connor MacLeod", + "Duncan MacLeod", + "Heather MacLeod", + "Joe Dawson", + "Kurgan, [bold italic red]The[/]", + "Methos", + "Rachel Ellenstein", + "Ramírez", + ) class Datatables(containers.VerticalGroup): @@ -309,6 +306,10 @@ class Logs(containers.VerticalGroup): } } TabPane { padding: 0; } + TabbedContent.-maximized { + height: 1fr; + Log, RichLog { height: 1fr; } + } } """ @@ -366,7 +367,7 @@ def on_mount(self) -> None: def update_log(self) -> None: """Update the Log with new content.""" log = self.query_one(Log) - if not self.screen.can_view_partial(log) or not self.screen.is_active: + if not self.app.screen.can_view_partial(log) and not log.is_in_maximized_view: return self.log_count += 1 line_no = self.log_count % len(self.TEXT) @@ -376,7 +377,10 @@ def update_log(self) -> None: def update_rich_log(self) -> None: """Update the Rich Log with content.""" rich_log = self.query_one(RichLog) - if not self.screen.can_view_partial(rich_log) or not self.screen.is_active: + if ( + not self.app.screen.can_view_partial(rich_log) + and not rich_log.is_in_maximized_view + ): return self.rich_log_count += 1 log_option = self.rich_log_count % 3 @@ -421,6 +425,11 @@ class Sparklines(containers.VerticalGroup): &#third > .sparkline--min-color { color: $primary; } &#third > .sparkline--max-color { color: $accent; } } + VerticalScroll { + height: auto; + border: heavy transparent; + &:focus { border: heavy $accent; } + } } """ @@ -430,22 +439,28 @@ class Sparklines(containers.VerticalGroup): def compose(self) -> ComposeResult: yield Markdown(self.LOGS_MD) - yield Sparkline([], summary_function=max, id="first").data_bind( - Sparklines.data, - ) - yield Sparkline([], summary_function=max, id="second").data_bind( - Sparklines.data, - ) - yield Sparkline([], summary_function=max, id="third").data_bind( - Sparklines.data, - ) + with containers.VerticalScroll( + id="container", can_focus=True, can_maximize=True + ): + yield Sparkline([], summary_function=max, id="first").data_bind( + Sparklines.data, + ) + yield Sparkline([], summary_function=max, id="second").data_bind( + Sparklines.data, + ) + yield Sparkline([], summary_function=max, id="third").data_bind( + Sparklines.data, + ) def on_mount(self) -> None: - self.set_interval(0.2, self.update_sparks) + self.set_interval(0.1, self.update_sparks) def update_sparks(self) -> None: """Update the sparks data.""" - if not self.screen.can_view_partial(self) or not self.screen.is_active: + if ( + not self.app.screen.can_view_partial(self) + and not self.query_one(Sparkline).is_in_maximized_view + ): return self.count += 1 offset = self.count * 40 diff --git a/src/textual/widget.py b/src/textual/widget.py index d251d06a62..55901636de 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -616,6 +616,17 @@ def is_maximized(self) -> bool: except NoScreen: return False + @property + def is_in_maximized_view(self) -> bool: + """Is this widget, or a parent maximized?""" + maximized = self.screen.maximized + if not maximized: + return False + for node in self.ancestors_with_self: + if maximized is node: + return True + return False + @property def _render_widget(self) -> Widget: """The widget the compositor should render.""" From adeb306ac90dcb96e2ffc146d022be626af34492 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 17:59:58 +0000 Subject: [PATCH 07/31] empty sparklines --- src/textual/demo/widgets.py | 4 ++-- src/textual/widgets/_sparkline.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index 00cce0ea72..d70d27abf1 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -367,7 +367,7 @@ def on_mount(self) -> None: def update_log(self) -> None: """Update the Log with new content.""" log = self.query_one(Log) - if not self.app.screen.can_view_partial(log) and not log.is_in_maximized_view: + if not self.app.screen.can_view_entire(log) and not log.is_in_maximized_view: return self.log_count += 1 line_no = self.log_count % len(self.TEXT) @@ -378,7 +378,7 @@ def update_rich_log(self) -> None: """Update the Rich Log with content.""" rich_log = self.query_one(RichLog) if ( - not self.app.screen.can_view_partial(rich_log) + not self.app.screen.can_view_entire(rich_log) and not rich_log.is_in_maximized_view ): return diff --git a/src/textual/widgets/_sparkline.py b/src/textual/widgets/_sparkline.py index 103cb08d94..0bc040c7a8 100644 --- a/src/textual/widgets/_sparkline.py +++ b/src/textual/widgets/_sparkline.py @@ -87,8 +87,9 @@ def __init__( def render(self) -> RenderResult: """Renders the sparkline when there is data available.""" - if not self.data: - return "" + data = self.data or [] + if not data: + return "" _, base = self.background_colors min_color = base + ( self.get_component_styles("sparkline--min-color").color @@ -101,7 +102,7 @@ def render(self) -> RenderResult: else self.max_color ) return SparklineRenderable( - self.data, + data, width=self.size.width, min_color=min_color.rich_color, max_color=max_color.rich_color, From 201f106055be3de12eee9147d4313e2a74595b77 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 18:39:16 +0000 Subject: [PATCH 08/31] test fixes --- src/textual/constants.py | 2 +- src/textual/pilot.py | 1 + tests/test_driver.py | 14 ++++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/textual/constants.py b/src/textual/constants.py index 7313fa614f..7cc88938fb 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -153,7 +153,7 @@ def _get_textual_animations() -> AnimationLevel: if message processing exceeds this duration. """ -DEFAULT_THEME: Final[str] = get_environ("TEXTUAL_THEME", "") +DEFAULT_THEME: Final[str] = get_environ("TEXTUAL_THEME", "textual-dark") """Textual theme to make default. More than one theme may be specified in a comma separated list. Textual will use the first theme that exists. """ diff --git a/src/textual/pilot.py b/src/textual/pilot.py index 63f3ac3db0..e7362ea4e6 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -32,6 +32,7 @@ def _get_mouse_message_arguments( """Get the arguments to pass into mouse messages for the click and hover methods.""" click_x, click_y = target.region.offset + offset message_arguments = { + "widget": target, "x": click_x, "y": click_y, "delta_x": 0, diff --git a/tests/test_driver.py b/tests/test_driver.py index 0c37f6c86f..764c9930da 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -18,8 +18,8 @@ def handle(self, event): app = MyApp() async with app.run_test() as pilot: - app._driver.process_message(MouseDown(0, 0, 0, 0, 1, False, False, False)) - app._driver.process_message(MouseUp(0, 0, 0, 0, 1, False, False, False)) + app._driver.process_message(MouseDown(None, 0, 0, 0, 0, 1, False, False, False)) + app._driver.process_message(MouseUp(None, 0, 0, 0, 0, 1, False, False, False)) await pilot.pause() assert len(app.messages) == 3 assert isinstance(app.messages[0], MouseDown) @@ -41,8 +41,8 @@ def on_button_pressed(self, event): app = MyApp() async with app.run_test() as pilot: - app._driver.process_message(MouseDown(0, 0, 0, 0, 1, False, False, False)) - app._driver.process_message(MouseUp(0, 0, 0, 0, 1, False, False, False)) + app._driver.process_message(MouseDown(None, 0, 0, 0, 0, 1, False, False, False)) + app._driver.process_message(MouseUp(None, 0, 0, 0, 0, 1, False, False, False)) await pilot.pause() assert len(app.messages) == 1 @@ -69,9 +69,10 @@ def on_button_pressed(self, event): assert (width, height) == (button_width, button_height) # Mouse down on the button, then move the mouse inside the button, then mouse up. - app._driver.process_message(MouseDown(0, 0, 0, 0, 1, False, False, False)) + app._driver.process_message(MouseDown(None, 0, 0, 0, 0, 1, False, False, False)) app._driver.process_message( MouseUp( + None, button_width - 1, button_height - 1, button_width - 1, @@ -108,9 +109,10 @@ def on_button_pressed(self, event): assert (width, height) == (button_width, button_height) # Mouse down on the button, then move the mouse outside the button, then mouse up. - app._driver.process_message(MouseDown(0, 0, 0, 0, 1, False, False, False)) + app._driver.process_message(MouseDown(None, 0, 0, 0, 0, 1, False, False, False)) app._driver.process_message( MouseUp( + None, button_width + 1, button_height + 1, button_width + 1, From 9f49bc2050abd25e4f6a9682b077c9c235d15ad5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 19:06:08 +0000 Subject: [PATCH 09/31] add is_scrolling --- CHANGELOG.md | 1 + src/textual/demo/widgets.py | 8 ++++++++ src/textual/widget.py | 13 +++++++++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd5f8ca3e3..a8feb789e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added several new built-in CSS variables https://github.com/Textualize/textual/pull/5087 - Added support for in-band terminal resize protocol https://github.com/Textualize/textual/pull/5217 - Added TEXTUAL_THEME environment var, which should be a comma separated list of desired themes +- Added `Widget.is_scrolling` ### Changed diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index d70d27abf1..cb0693cf5a 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -367,6 +367,8 @@ def on_mount(self) -> None: def update_log(self) -> None: """Update the Log with new content.""" log = self.query_one(Log) + if self.is_scrolling: + return if not self.app.screen.can_view_entire(log) and not log.is_in_maximized_view: return self.log_count += 1 @@ -377,6 +379,8 @@ def update_log(self) -> None: def update_rich_log(self) -> None: """Update the Rich Log with content.""" rich_log = self.query_one(RichLog) + if self.is_scrolling: + return if ( not self.app.screen.can_view_entire(rich_log) and not rich_log.is_in_maximized_view @@ -457,6 +461,8 @@ def on_mount(self) -> None: def update_sparks(self) -> None: """Update the sparks data.""" + if self.is_scrolling: + return if ( not self.app.screen.can_view_partial(self) and not self.query_one(Sparkline).is_in_maximized_view @@ -490,6 +496,8 @@ def compose(self) -> ComposeResult: yield Markdown(self.SWITCHES_MD) with containers.ItemGrid(min_column_width=32): for theme in BUILTIN_THEMES: + if theme.endswith("-ansi"): + continue with containers.HorizontalGroup(): yield Switch(id=theme) yield Label(theme, name=theme) diff --git a/src/textual/widget.py b/src/textual/widget.py index 55901636de..aa48af8e06 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2208,6 +2208,19 @@ def is_scrollable(self) -> bool: """Can this widget be scrolled?""" return self.styles.layout is not None or bool(self._nodes) + @property + def is_scrolling(self) -> bool: + """Is this widget currently scrolling?""" + for node in self.ancestors: + if not isinstance(node, Widget): + break + if ( + node.scroll_x != node.scroll_target_x + or node.scroll_y != node.scroll_target_y + ): + return True + return False + @property def layer(self) -> str: """Get the name of this widgets layer. From 6fadab901f12ddd95936dd2398585a3c12a2ce51 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 19:21:02 +0000 Subject: [PATCH 10/31] remove scroll pause --- src/textual/app.py | 36 ++---------------------------------- src/textual/widget.py | 3 --- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 92147561ff..8b7b4a638d 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -462,7 +462,7 @@ class MyApp(App[None]): SUSPENDED_SCREEN_CLASS: ClassVar[str] = "" """Class to apply to suspended screens, or empty string for no class.""" - HOVER_EFFECTS_SCROLL_PAUSE: ClassVar[float] = 0.2 + HOVER_EFFECTS_SCROLL_PAUSE: ClassVar[float] = 0.02 """Seconds to pause hover effects for when scrolling.""" _PSEUDO_CLASSES: ClassVar[dict[str, Callable[[App[Any]], bool]]] = { @@ -757,9 +757,6 @@ def __init__( self._previous_inline_height: int | None = None """Size of previous inline update.""" - self._paused_hover_effects: bool = False - """Have the hover effects been paused?""" - self._hover_effects_timer: Timer | None = None self._resize_event: events.Resize | None = None @@ -2814,42 +2811,13 @@ def set_focus(self, widget: Widget | None, scroll_visible: bool = True) -> None: """ self.screen.set_focus(widget, scroll_visible) - def _pause_hover_effects(self): - """Pause any hover effects based on Enter and Leave events for 200ms.""" - if not self.HOVER_EFFECTS_SCROLL_PAUSE or self.is_headless: - return - self._paused_hover_effects = True - if self._hover_effects_timer is None: - self._hover_effects_timer = self.set_interval( - self.HOVER_EFFECTS_SCROLL_PAUSE, self._resume_hover_effects - ) - else: - self._hover_effects_timer.reset() - self._hover_effects_timer.resume() - - def _resume_hover_effects(self): - """Resume sending Enter and Leave for hover effects.""" - if not self.HOVER_EFFECTS_SCROLL_PAUSE or self.is_headless: - return - if self._paused_hover_effects: - self._paused_hover_effects = False - if self._hover_effects_timer is not None: - self._hover_effects_timer.pause() - try: - widget, _ = self.screen.get_widget_at(*self.mouse_position) - except NoWidget: - pass - else: - if widget is not self.mouse_over: - self._set_mouse_over(widget) - def _set_mouse_over(self, widget: Widget | None) -> None: """Called when the mouse is over another widget. Args: widget: Widget under mouse, or None for no widgets. """ - if self._paused_hover_effects: + if widget is not None and widget.is_scrolling: return if widget is None: if self.mouse_over is not None: diff --git a/src/textual/widget.py b/src/textual/widget.py index aa48af8e06..a6fb1697f9 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2412,9 +2412,6 @@ def _scroll_to( if on_complete is not None: self.call_after_refresh(on_complete) - if scrolled_x or scrolled_y: - self.app._pause_hover_effects() - return scrolled_x or scrolled_y def pre_layout(self, layout: Layout) -> None: From 7c1ebfce1d783c122721798a18feaefa4406dbcf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 19:24:17 +0000 Subject: [PATCH 11/31] button tweak --- src/textual/widgets/_button.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 49b436cf82..acbf15e786 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -60,7 +60,8 @@ class Button(Widget, can_focus=True): text-style: bold; &:disabled { - text-style: not bold; + text-style: bold; + text-opacity: 0.5; } &:focus { From 35d6d6ab53cf5fb13a90565a0ec0cf78e4dbeb52 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 19:25:28 +0000 Subject: [PATCH 12/31] restore previous buttons --- src/textual/widgets/_button.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index acbf15e786..49b436cf82 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -60,8 +60,7 @@ class Button(Widget, can_focus=True): text-style: bold; &:disabled { - text-style: bold; - text-opacity: 0.5; + text-style: not bold; } &:focus { From d31b373d5d3301012507620823efe55f0d3a7bbe Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 21:04:31 +0000 Subject: [PATCH 13/31] snapshot fixes --- CHANGELOG.md | 7 +- src/textual/command.py | 1 + src/textual/renderables/digits.py | 2 +- src/textual/widgets/_list_item.py | 11 +- src/textual/widgets/_list_view.py | 10 +- .../test_ansi_command_palette.svg | 134 +++++++-------- ...ommands_opens_and_displays_search_list.svg | 124 +++++++------- .../test_snapshots/test_command_palette.svg | 124 +++++++------- .../test_command_palette_discovery.svg | 124 +++++++------- .../test_snapshots/test_digits.svg | 112 ++++++------- .../test_snapshots/test_system_commands.svg | 152 +++++++++--------- 11 files changed, 406 insertions(+), 395 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8feb789e4..66e6ccf7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,12 +39,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226 - Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226 - `Click` events can now be used with the on decorator to match the initially clicked widget - -### Changed - - Breaking change: Removed `App.dark` reactive attribute https://github.com/Textualize/textual/pull/5087 - Breaking change: To improve consistency, several changes have been made to default widget CSS and the CSS variables which ship with Textual. On upgrading, your app will likely look different. All of these changes can be overidden with your own CSS. https://github.com/Textualize/textual/pull/5087 +### Removed + +- Removed `App.HOVER_EFFECTS_SCROLL_PAUSE` + ## [0.85.2] - 2024-11-02 - Fixed broken focus-within https://github.com/Textualize/textual/pull/5190 diff --git a/src/textual/command.py b/src/textual/command.py index 87d49c80ab..68677ba001 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -571,6 +571,7 @@ class CommandPalette(SystemModalScreen[None]): height: 100%; visibility: hidden; background: $surface; + &:dark { background: $panel-darken-1; } } CommandPalette #--input { diff --git a/src/textual/renderables/digits.py b/src/textual/renderables/digits.py index c6d982f5dc..e48a494dd3 100644 --- a/src/textual/renderables/digits.py +++ b/src/textual/renderables/digits.py @@ -13,7 +13,7 @@ ┏━┓ ┃ ┃ ┗━┛ - ┓ +╺┓ ┃ ╺┻╸ ╺━┓ diff --git a/src/textual/widgets/_list_item.py b/src/textual/widgets/_list_item.py index 8e889d6427..0df949c1be 100644 --- a/src/textual/widgets/_list_item.py +++ b/src/textual/widgets/_list_item.py @@ -2,7 +2,7 @@ from __future__ import annotations -from textual import events +from textual import events, on from textual.message import Message from textual.reactive import reactive from textual.widget import Widget @@ -30,5 +30,10 @@ def _on_click(self, _: events.Click) -> None: self.post_message(self._ChildClicked(self)) def watch_highlighted(self, value: bool) -> None: - print("highlighted", value) - self.set_class(value, "--highlight") + self.set_class(value, "-highlight") + + @on(events.Enter) + @on(events.Leave) + def on_enter_or_leave(self, event: events.Enter | events.Leave) -> None: + event.stop() + self.set_class(self.is_mouse_over, "-hovered") diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index dea43c291f..e2363a80f8 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -39,19 +39,23 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False): height: auto; overflow: hidden hidden; width: 1fr; + + &.-hovered { + background: $block-hover-background; + } - &.--highlight > Widget { + &.-highlight { color: $block-cursor-blurred-foreground; background: $block-cursor-blurred-background; text-style: $block-cursor-blurred-text-style; } } - &:focus > ListItem.--highlight > Widget { + &:focus > ListItem.-highlight > Widget { width: 1fr; color: $block-cursor-foreground; background: $block-cursor-background; - text-style: $block-cursor-text-style; + text-style: $block-cursor-text-style; } } """ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg index f720ab0264..806fe837e3 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_ansi_command_palette.svg @@ -19,143 +19,143 @@ font-weight: 700; } - .terminal-1758253465-matrix { + .terminal-2368587539-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1758253465-title { + .terminal-2368587539-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1758253465-r1 { fill: #8a4346 } -.terminal-1758253465-r2 { fill: #868887 } -.terminal-1758253465-r3 { fill: #6b546f } -.terminal-1758253465-r4 { fill: #e0e0e0 } -.terminal-1758253465-r5 { fill: #292929 } -.terminal-1758253465-r6 { fill: #c5c8c6 } -.terminal-1758253465-r7 { fill: #0178d4 } -.terminal-1758253465-r8 { fill: #00ff00 } -.terminal-1758253465-r9 { fill: #000000 } -.terminal-1758253465-r10 { fill: #8d8d8d } -.terminal-1758253465-r11 { fill: #828482 } -.terminal-1758253465-r12 { fill: #e0e0e0;font-weight: bold } -.terminal-1758253465-r13 { fill: #a5a5a5 } + .terminal-2368587539-r1 { fill: #8a4346 } +.terminal-2368587539-r2 { fill: #868887 } +.terminal-2368587539-r3 { fill: #6b546f } +.terminal-2368587539-r4 { fill: #e0e0e0 } +.terminal-2368587539-r5 { fill: #292929 } +.terminal-2368587539-r6 { fill: #c5c8c6 } +.terminal-2368587539-r7 { fill: #0178d4 } +.terminal-2368587539-r8 { fill: #00ff00 } +.terminal-2368587539-r9 { fill: #000000 } +.terminal-2368587539-r10 { fill: #8d8d8d } +.terminal-2368587539-r11 { fill: #7e8486 } +.terminal-2368587539-r12 { fill: #e0e0e0;font-weight: bold } +.terminal-2368587539-r13 { fill: #a1a5a8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CommandPaletteApp + CommandPaletteApp - - - - RedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  Quit the application                                                           -Quit the application as soon as possible -  Save screenshot                                                                -Save an SVG 'screenshot' of the current screen -  Show keys and help panel                                                       -Show help for the focused widget and a summary of available keys -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed -MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed + + + + RedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  Quit the application                                                           +Quit the application as soon as possible +  Save screenshot                                                                +Save an SVG 'screenshot' of the current screen +  Show keys and help panel                                                       +Show help for the focused widget and a summary of available keys +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed +MagentaRedMagentaRedMagentaRedMagentaRedMagentaRedMagentaRed diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg index eb86226537..89e9f90784 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_app_search_commands_opens_and_displays_search_list.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-4097857637-matrix { + .terminal-1636454365-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4097857637-title { + .terminal-1636454365-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4097857637-r1 { fill: #646464 } -.terminal-4097857637-r2 { fill: #c5c8c6 } -.terminal-4097857637-r3 { fill: #0178d4 } -.terminal-4097857637-r4 { fill: #e0e0e0 } -.terminal-4097857637-r5 { fill: #00ff00 } -.terminal-4097857637-r6 { fill: #000000 } -.terminal-4097857637-r7 { fill: #121212 } -.terminal-4097857637-r8 { fill: #e0e0e0;font-weight: bold } -.terminal-4097857637-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } + .terminal-1636454365-r1 { fill: #646464 } +.terminal-1636454365-r2 { fill: #c5c8c6 } +.terminal-1636454365-r3 { fill: #0178d4 } +.terminal-1636454365-r4 { fill: #e0e0e0 } +.terminal-1636454365-r5 { fill: #00ff00 } +.terminal-1636454365-r6 { fill: #000000 } +.terminal-1636454365-r7 { fill: #121212 } +.terminal-1636454365-r8 { fill: #e0e0e0;font-weight: bold } +.terminal-1636454365-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SearchApp + SearchApp - - - - Search Commands                                                                  - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎b - - -bar                                                                            -baz                                                                            -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - + + + + Search Commands                                                                  + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎b + + +bar                                                                            +baz                                                                            +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg index 03936e6c98..cba485475e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-621274872-matrix { + .terminal-566941328-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-621274872-title { + .terminal-566941328-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-621274872-r1 { fill: #646464 } -.terminal-621274872-r2 { fill: #c5c8c6 } -.terminal-621274872-r3 { fill: #0178d4 } -.terminal-621274872-r4 { fill: #e0e0e0 } -.terminal-621274872-r5 { fill: #00ff00 } -.terminal-621274872-r6 { fill: #000000 } -.terminal-621274872-r7 { fill: #121212 } -.terminal-621274872-r8 { fill: #e0e0e0;font-weight: bold } -.terminal-621274872-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } + .terminal-566941328-r1 { fill: #646464 } +.terminal-566941328-r2 { fill: #c5c8c6 } +.terminal-566941328-r3 { fill: #0178d4 } +.terminal-566941328-r4 { fill: #e0e0e0 } +.terminal-566941328-r5 { fill: #00ff00 } +.terminal-566941328-r6 { fill: #000000 } +.terminal-566941328-r7 { fill: #121212 } +.terminal-566941328-r8 { fill: #e0e0e0;font-weight: bold } +.terminal-566941328-r9 { fill: #e0e0e0;font-weight: bold;text-decoration: underline; } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CommandPaletteApp + CommandPaletteApp - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎A - - -  This is a test of this code 9                                                  -  This is a test of this code 8                                                  -  This is a test of this code 7                                                  -  This is a test of this code 6                                                  -  This is a test of this code 5                                                  -  This is a test of this code 4                                                  -  This is a test of this code 3                                                  -  This is a test of this code 2                                                  -  This is a test of this code 1                                                  -  This is a test of this code 0                                                  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎A + + +  This is a test of this code 9                                                  +  This is a test of this code 8                                                  +  This is a test of this code 7                                                  +  This is a test of this code 6                                                  +  This is a test of this code 5                                                  +  This is a test of this code 4                                                  +  This is a test of this code 3                                                  +  This is a test of this code 2                                                  +  This is a test of this code 1                                                  +  This is a test of this code 0                                                  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg index 94dc22828b..60c08e5cee 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_command_palette_discovery.svg @@ -19,138 +19,138 @@ font-weight: 700; } - .terminal-2694158592-matrix { + .terminal-1531744096-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2694158592-title { + .terminal-1531744096-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2694158592-r1 { fill: #646464 } -.terminal-2694158592-r2 { fill: #c5c8c6 } -.terminal-2694158592-r3 { fill: #0178d4 } -.terminal-2694158592-r4 { fill: #e0e0e0 } -.terminal-2694158592-r5 { fill: #00ff00 } -.terminal-2694158592-r6 { fill: #000000 } -.terminal-2694158592-r7 { fill: #121212 } -.terminal-2694158592-r8 { fill: #737373 } -.terminal-2694158592-r9 { fill: #e0e0e0;font-weight: bold } + .terminal-1531744096-r1 { fill: #646464 } +.terminal-1531744096-r2 { fill: #c5c8c6 } +.terminal-1531744096-r3 { fill: #0178d4 } +.terminal-1531744096-r4 { fill: #e0e0e0 } +.terminal-1531744096-r5 { fill: #00ff00 } +.terminal-1531744096-r6 { fill: #000000 } +.terminal-1531744096-r7 { fill: #121212 } +.terminal-1531744096-r8 { fill: #6d7479 } +.terminal-1531744096-r9 { fill: #e0e0e0;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CommandPaletteApp + CommandPaletteApp - - - - - - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  This is a test of this code 0                                                  -  This is a test of this code 1                                                  -  This is a test of this code 2                                                  -  This is a test of this code 3                                                  -  This is a test of this code 4                                                  -  This is a test of this code 5                                                  -  This is a test of this code 6                                                  -  This is a test of this code 7                                                  -  This is a test of this code 8                                                  -  This is a test of this code 9                                                  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - + + + + + + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  This is a test of this code 0                                                  +  This is a test of this code 1                                                  +  This is a test of this code 2                                                  +  This is a test of this code 3                                                  +  This is a test of this code 4                                                  +  This is a test of this code 5                                                  +  This is a test of this code 6                                                  +  This is a test of this code 7                                                  +  This is a test of this code 8                                                  +  This is a test of this code 9                                                  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg index 1adf5d0eb8..3c9653bda3 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-3814412032-matrix { + .terminal-1646678289-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3814412032-title { + .terminal-1646678289-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3814412032-r1 { fill: #e0e0e0 } -.terminal-3814412032-r2 { fill: #c5c8c6 } -.terminal-3814412032-r3 { fill: #e0e0e0;font-weight: bold } + .terminal-1646678289-r1 { fill: #e0e0e0 } +.terminal-1646678289-r2 { fill: #c5c8c6 } +.terminal-1646678289-r3 { fill: #e0e0e0;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DigitApp + DigitApp - + - - ╶─╮ ╶╮ ╷ ╷╶╮ ╭─╴╭─╮╶─╮╭─╴╭─╴╶─╮╭─╴╭─╮                                            - ─┤  │ ╰─┤ │ ╰─╮╰─┤┌─┘├─╮╰─╮ ─┤╰─╮╰─┤                                            -╶─╯•╶┴╴  ╵╶┴╴╶─╯╶─╯╰─╴╰─╯╶─╯╶─╯╶─╯╶─╯                                            -             ╭─╮╶╮ ╶─╮╶─╮╷ ╷╭─╴╭─╴╶─┐╭─╮╭─╮        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            -             │ │ │ ┌─┘ ─┤╰─┤╰─╮├─╮  │├─┤╰─┤╶┼╴╶─╴  ├─┤├─┤│  │ │├─ ├─             -             ╰─╯╶┴╴╰─╴╶─╯  ╵╶─╯╰─╯  ╵╰─╯╶─╯      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              -             ┏━┓ ┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            -             ┃ ┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ┃┣━┫┗━┫╺╋╸╺━╸  ├─┤├─┤│  │ │├─ ├─             -             ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛  ╹┗━┛╺━┛      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              -                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ -                                                               ─┤ ×  │ │ │   ╰─┤ -                                                              ╶─╯   ╶┴╴╰─╯     ╵ -                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ -                                                               ─┤ ×  │ │ │   ╰─┤ -                                                              ╶─╯   ╶┴╴╰─╯     ╵ -╭╴ ╭╫╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴ ╶╮                                                        -│  ╰╫╮ │ ┌─┘ ─┤ ╰─┤╰─╮  │                                                        -╰╴ ╰╫╯╶┴╴╰─╴╶─╯•  ╵╶─╯ ╶╯                                                        -╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              -╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              -┴─╴╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              -╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              -╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              -╰─╯╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              + + ╶─╮ ╶╮ ╷ ╷╶╮ ╭─╴╭─╮╶─╮╭─╴╭─╴╶─╮╭─╴╭─╮                                            + ─┤  │ ╰─┤ │ ╰─╮╰─┤┌─┘├─╮╰─╮ ─┤╰─╮╰─┤                                            +╶─╯•╶┴╴  ╵╶┴╴╶─╯╶─╯╰─╴╰─╯╶─╯╶─╯╶─╯╶─╯                                            +             ╭─╮╶╮ ╶─╮╶─╮╷ ╷╭─╴╭─╴╶─┐╭─╮╭─╮        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            +             │ │ │ ┌─┘ ─┤╰─┤╰─╮├─╮  │├─┤╰─┤╶┼╴╶─╴  ├─┤├─┤│  │ │├─ ├─             +             ╰─╯╶┴╴╰─╴╶─╯  ╵╶─╯╰─╯  ╵╰─╯╶─╯      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              +             ┏━┓╺┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            +             ┃ ┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ┃┣━┫┗━┫╺╋╸╺━╸  ├─┤├─┤│  │ │├─ ├─             +             ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛  ╹┗━┛╺━┛      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              +                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ +                                                               ─┤ ×  │ │ │   ╰─┤ +                                                              ╶─╯   ╶┴╴╰─╯     ╵ +                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ +                                                               ─┤ ×  │ │ │   ╰─┤ +                                                              ╶─╯   ╶┴╴╰─╯     ╵ +╭╴ ╭╫╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴ ╶╮                                                        +│  ╰╫╮ │ ┌─┘ ─┤ ╰─┤╰─╮  │                                                        +╰╴ ╰╫╯╶┴╴╰─╴╶─╯•  ╵╶─╯ ╶╯                                                        +╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              +╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              +┴─╴╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              +╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              +╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              +╰─╯╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg index 3f88acaab0..dbb425da5e 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_system_commands.svg @@ -19,164 +19,164 @@ font-weight: 700; } - .terminal-4115527248-matrix { + .terminal-3076581726-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-4115527248-title { + .terminal-3076581726-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-4115527248-r1 { fill: #121212 } -.terminal-4115527248-r2 { fill: #0b3a5f } -.terminal-4115527248-r3 { fill: #c5c8c6 } -.terminal-4115527248-r4 { fill: #e0e0e0 } -.terminal-4115527248-r5 { fill: #0178d4 } -.terminal-4115527248-r6 { fill: #00ff00 } -.terminal-4115527248-r7 { fill: #000000 } -.terminal-4115527248-r8 { fill: #737373 } -.terminal-4115527248-r9 { fill: #e0e0e0;font-weight: bold } -.terminal-4115527248-r10 { fill: #a5a5a5 } -.terminal-4115527248-r11 { fill: #646464 } + .terminal-3076581726-r1 { fill: #121212 } +.terminal-3076581726-r2 { fill: #0b3a5f } +.terminal-3076581726-r3 { fill: #c5c8c6 } +.terminal-3076581726-r4 { fill: #e0e0e0 } +.terminal-3076581726-r5 { fill: #0178d4 } +.terminal-3076581726-r6 { fill: #00ff00 } +.terminal-3076581726-r7 { fill: #000000 } +.terminal-3076581726-r8 { fill: #6d7479 } +.terminal-3076581726-r9 { fill: #e0e0e0;font-weight: bold } +.terminal-3076581726-r10 { fill: #a1a5a8 } +.terminal-3076581726-r11 { fill: #646464 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - SimpleApp + SimpleApp - - - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - -🔎Search for commands… - - -  Change theme                                                                                       -Change the current theme -  Maximize                                                                                           -Maximize the focused widget -  Quit the application                                                                               -Quit the application as soon as possible -  Save screenshot                                                                                    -Save an SVG 'screenshot' of the current screen -  Show keys and help panel                                                                           -Show help for the focused widget and a summary of available keys -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + +🔎Search for commands… + + +  Change theme                                                                                       +Change the current theme +  Maximize                                                                                           +Maximize the focused widget +  Quit the application                                                                               +Quit the application as soon as possible +  Save screenshot                                                                                    +Save an SVG 'screenshot' of the current screen +  Show keys and help panel                                                                           +Show help for the focused widget and a summary of available keys +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + From 75433036e9d1c3b65f27107ad24151b90a79f8ff Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 21:08:34 +0000 Subject: [PATCH 14/31] digit --- src/textual/renderables/digits.py | 4 +- .../test_snapshots/test_digits.svg | 112 +++++++++--------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/textual/renderables/digits.py b/src/textual/renderables/digits.py index e48a494dd3..bcea3ed612 100644 --- a/src/textual/renderables/digits.py +++ b/src/textual/renderables/digits.py @@ -78,7 +78,7 @@ ╰╫╯ ╭─╮ ╪═ -┴─╴ +┷━╸ ╭─╮ ╪═ ╰─╯ @@ -163,7 +163,7 @@ ╰╫╯ ╭─╮ ╪═ -┴─╴ +┷━╸ ╭─╮ ╪═ ╰─╯ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg index 3c9653bda3..d1065cf748 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_digits.svg @@ -19,133 +19,133 @@ font-weight: 700; } - .terminal-1646678289-matrix { + .terminal-2360037657-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1646678289-title { + .terminal-2360037657-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1646678289-r1 { fill: #e0e0e0 } -.terminal-1646678289-r2 { fill: #c5c8c6 } -.terminal-1646678289-r3 { fill: #e0e0e0;font-weight: bold } + .terminal-2360037657-r1 { fill: #e0e0e0 } +.terminal-2360037657-r2 { fill: #c5c8c6 } +.terminal-2360037657-r3 { fill: #e0e0e0;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - DigitApp + DigitApp - + - - ╶─╮ ╶╮ ╷ ╷╶╮ ╭─╴╭─╮╶─╮╭─╴╭─╴╶─╮╭─╴╭─╮                                            - ─┤  │ ╰─┤ │ ╰─╮╰─┤┌─┘├─╮╰─╮ ─┤╰─╮╰─┤                                            -╶─╯•╶┴╴  ╵╶┴╴╶─╯╶─╯╰─╴╰─╯╶─╯╶─╯╶─╯╶─╯                                            -             ╭─╮╶╮ ╶─╮╶─╮╷ ╷╭─╴╭─╴╶─┐╭─╮╭─╮        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            -             │ │ │ ┌─┘ ─┤╰─┤╰─╮├─╮  │├─┤╰─┤╶┼╴╶─╴  ├─┤├─┤│  │ │├─ ├─             -             ╰─╯╶┴╴╰─╴╶─╯  ╵╶─╯╰─╯  ╵╰─╯╶─╯      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              -             ┏━┓╺┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            -             ┃ ┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ┃┣━┫┗━┫╺╋╸╺━╸  ├─┤├─┤│  │ │├─ ├─             -             ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛  ╹┗━┛╺━┛      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              -                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ -                                                               ─┤ ×  │ │ │   ╰─┤ -                                                              ╶─╯   ╶┴╴╰─╯     ╵ -                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ -                                                               ─┤ ×  │ │ │   ╰─┤ -                                                              ╶─╯   ╶┴╴╰─╯     ╵ -╭╴ ╭╫╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴ ╶╮                                                        -│  ╰╫╮ │ ┌─┘ ─┤ ╰─┤╰─╮  │                                                        -╰╴ ╰╫╯╶┴╴╰─╴╶─╯•  ╵╶─╯ ╶╯                                                        -╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              -╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              -┴─╴╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              -╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              -╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              -╰─╯╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              + + ╶─╮ ╶╮ ╷ ╷╶╮ ╭─╴╭─╮╶─╮╭─╴╭─╴╶─╮╭─╴╭─╮                                            + ─┤  │ ╰─┤ │ ╰─╮╰─┤┌─┘├─╮╰─╮ ─┤╰─╮╰─┤                                            +╶─╯•╶┴╴  ╵╶┴╴╶─╯╶─╯╰─╴╰─╯╶─╯╶─╯╶─╯╶─╯                                            +             ╭─╮╶╮ ╶─╮╶─╮╷ ╷╭─╴╭─╴╶─┐╭─╮╭─╮        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            +             │ │ │ ┌─┘ ─┤╰─┤╰─╮├─╮  │├─┤╰─┤╶┼╴╶─╴  ├─┤├─┤│  │ │├─ ├─             +             ╰─╯╶┴╴╰─╴╶─╯  ╵╶─╯╰─╯  ╵╰─╯╶─╯      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              +             ┏━┓╺┓ ╺━┓╺━┓╻ ╻┏━╸┏━╸╺━┓┏━┓┏━┓        ╭─╮┌─╮╭─╮┌─╮╭─╴╭─╴            +             ┃ ┃ ┃ ┏━┛ ━┫┗━┫┗━┓┣━┓  ┃┣━┫┗━┫╺╋╸╺━╸  ├─┤├─┤│  │ │├─ ├─             +             ┗━┛╺┻╸┗━╸╺━┛  ╹╺━┛┗━┛  ╹┗━┛╺━┛      •,╵ ╵└─╯╰─╯└─╯╰─╴╵              +                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ +                                                               ─┤ ×  │ │ │   ╰─┤ +                                                              ╶─╯   ╶┴╴╰─╯     ╵ +                                                              ╶─╮   ╶╮ ╭─╮ ^ ╷ ╷ +                                                               ─┤ ×  │ │ │   ╰─┤ +                                                              ╶─╯   ╶┴╴╰─╯     ╵ +╭╴ ╭╫╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴ ╶╮                                                        +│  ╰╫╮ │ ┌─┘ ─┤ ╰─┤╰─╮  │                                                        +╰╴ ╰╫╯╶┴╴╰─╴╶─╯•  ╵╶─╯ ╶╯                                                        +╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              +╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              +┷━╸╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              +╭─╮╶╮ ╶─╮╶─╮ ╷ ╷╭─╴                                                              +╪═  │ ┌─┘ ─┤ ╰─┤╰─╮                                                              +╰─╯╶┴╴╰─╴╶─╯•  ╵╶─╯                                                              From a5c3c70596cc7e0c31e73429e740fea7d9e105fa Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 14 Nov 2024 21:15:19 +0000 Subject: [PATCH 15/31] disable button text --- src/textual/widgets/_button.py | 4 +- .../test_button_with_console_markup.svg | 128 ++++++------- .../test_snapshots/test_buttons_render.svg | 170 +++++++++--------- .../test_snapshots/test_disabled_widgets.svg | 170 +++++++++--------- .../test_programmatic_disable_button.svg | 120 ++++++------- ...test_tabbed_content_with_modified_tabs.svg | 124 ++++++------- 6 files changed, 358 insertions(+), 358 deletions(-) diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 49b436cf82..20b4572207 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -59,8 +59,8 @@ class Button(Widget, can_focus=True): content-align: center middle; text-style: bold; - &:disabled { - text-style: not bold; + &:disabled { + text-opacity: 0.6; } &:focus { diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_button_with_console_markup.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_button_with_console_markup.svg index bf38bc9d0e..d264449edf 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_button_with_console_markup.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_button_with_console_markup.svg @@ -19,141 +19,141 @@ font-weight: 700; } - .terminal-3330596708-matrix { + .terminal-1193991823-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3330596708-title { + .terminal-1193991823-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3330596708-r1 { fill: #2d2d2d } -.terminal-3330596708-r2 { fill: #e0e0e0 } -.terminal-3330596708-r3 { fill: #c5c8c6 } -.terminal-3330596708-r4 { fill: #272727;font-weight: bold } -.terminal-3330596708-r5 { fill: #272727;font-weight: bold;font-style: italic; } -.terminal-3330596708-r6 { fill: #0d0d0d } -.terminal-3330596708-r7 { fill: #e0e0e0;font-weight: bold } -.terminal-3330596708-r8 { fill: #f4005f;font-weight: bold;font-style: italic; } -.terminal-3330596708-r9 { fill: #1e1e1e } -.terminal-3330596708-r10 { fill: #a2a2a2 } -.terminal-3330596708-r11 { fill: #5f0505;font-style: italic; } -.terminal-3330596708-r12 { fill: #0f0f0f } + .terminal-1193991823-r1 { fill: #2d2d2d } +.terminal-1193991823-r2 { fill: #e0e0e0 } +.terminal-1193991823-r3 { fill: #c5c8c6 } +.terminal-1193991823-r4 { fill: #272727;font-weight: bold } +.terminal-1193991823-r5 { fill: #272727;font-weight: bold;font-style: italic; } +.terminal-1193991823-r6 { fill: #0d0d0d } +.terminal-1193991823-r7 { fill: #e0e0e0;font-weight: bold } +.terminal-1193991823-r8 { fill: #f4005f;font-weight: bold;font-style: italic; } +.terminal-1193991823-r9 { fill: #1e1e1e } +.terminal-1193991823-r10 { fill: #6a6a6a;font-weight: bold } +.terminal-1193991823-r11 { fill: #5f0505;font-weight: bold;font-style: italic; } +.terminal-1193991823-r12 { fill: #0f0f0f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ButtonsWithMarkupApp + ButtonsWithMarkupApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Focused Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Blurred Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ -Disabled Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Focused Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Blurred Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +Disabled Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_buttons_render.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_buttons_render.svg index b6cfb0fa78..3585fe3987 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_buttons_render.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_buttons_render.svg @@ -19,162 +19,162 @@ font-weight: 700; } - .terminal-3544116530-matrix { + .terminal-622193266-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3544116530-title { + .terminal-622193266-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3544116530-r1 { fill: #e0e0e0 } -.terminal-3544116530-r2 { fill: #c5c8c6 } -.terminal-3544116530-r3 { fill: #e0e0e0;font-weight: bold } -.terminal-3544116530-r4 { fill: #2d2d2d } -.terminal-3544116530-r5 { fill: #1e1e1e } -.terminal-3544116530-r6 { fill: #272727;font-weight: bold } -.terminal-3544116530-r7 { fill: #a2a2a2 } -.terminal-3544116530-r8 { fill: #0d0d0d } -.terminal-3544116530-r9 { fill: #0f0f0f } -.terminal-3544116530-r10 { fill: #6db2ff } -.terminal-3544116530-r11 { fill: #3e6085 } -.terminal-3544116530-r12 { fill: #ddedf9;font-weight: bold } -.terminal-3544116530-r13 { fill: #a0a8ae } -.terminal-3544116530-r14 { fill: #004295 } -.terminal-3544116530-r15 { fill: #082951 } -.terminal-3544116530-r16 { fill: #7ae998 } -.terminal-3544116530-r17 { fill: #447b53 } -.terminal-3544116530-r18 { fill: #0a180e;font-weight: bold } -.terminal-3544116530-r19 { fill: #0a120c } -.terminal-3544116530-r20 { fill: #008139 } -.terminal-3544116530-r21 { fill: #084724 } -.terminal-3544116530-r22 { fill: #ffcf56 } -.terminal-3544116530-r23 { fill: #856e32 } -.terminal-3544116530-r24 { fill: #211505;font-weight: bold } -.terminal-3544116530-r25 { fill: #150f08 } -.terminal-3544116530-r26 { fill: #b86b00 } -.terminal-3544116530-r27 { fill: #633d08 } -.terminal-3544116530-r28 { fill: #e76580 } -.terminal-3544116530-r29 { fill: #7a3a47 } -.terminal-3544116530-r30 { fill: #f5e5e9;font-weight: bold } -.terminal-3544116530-r31 { fill: #aca4a6 } -.terminal-3544116530-r32 { fill: #780028 } -.terminal-3544116530-r33 { fill: #43081c } + .terminal-622193266-r1 { fill: #e0e0e0 } +.terminal-622193266-r2 { fill: #c5c8c6 } +.terminal-622193266-r3 { fill: #e0e0e0;font-weight: bold } +.terminal-622193266-r4 { fill: #2d2d2d } +.terminal-622193266-r5 { fill: #1e1e1e } +.terminal-622193266-r6 { fill: #272727;font-weight: bold } +.terminal-622193266-r7 { fill: #6a6a6a;font-weight: bold } +.terminal-622193266-r8 { fill: #0d0d0d } +.terminal-622193266-r9 { fill: #0f0f0f } +.terminal-622193266-r10 { fill: #6db2ff } +.terminal-622193266-r11 { fill: #3e6085 } +.terminal-622193266-r12 { fill: #ddedf9;font-weight: bold } +.terminal-622193266-r13 { fill: #637f94;font-weight: bold } +.terminal-622193266-r14 { fill: #004295 } +.terminal-622193266-r15 { fill: #082951 } +.terminal-622193266-r16 { fill: #7ae998 } +.terminal-622193266-r17 { fill: #447b53 } +.terminal-622193266-r18 { fill: #0a180e;font-weight: bold } +.terminal-622193266-r19 { fill: #193320;font-weight: bold } +.terminal-622193266-r20 { fill: #008139 } +.terminal-622193266-r21 { fill: #084724 } +.terminal-622193266-r22 { fill: #ffcf56 } +.terminal-622193266-r23 { fill: #856e32 } +.terminal-622193266-r24 { fill: #211505;font-weight: bold } +.terminal-622193266-r25 { fill: #422d10;font-weight: bold } +.terminal-622193266-r26 { fill: #b86b00 } +.terminal-622193266-r27 { fill: #633d08 } +.terminal-622193266-r28 { fill: #e76580 } +.terminal-622193266-r29 { fill: #7a3a47 } +.terminal-622193266-r30 { fill: #f5e5e9;font-weight: bold } +.terminal-622193266-r31 { fill: #8f7178;font-weight: bold } +.terminal-622193266-r32 { fill: #780028 } +.terminal-622193266-r33 { fill: #43081c } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ButtonsApp + ButtonsApp - + - - -Standard ButtonsDisabled Buttons - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Default  Default  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Primary!  Primary!  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Success!  Success!  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Warning!  Warning!  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Error!  Error!  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + +Standard ButtonsDisabled Buttons + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Default  Default  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Primary!  Primary!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Success!  Success!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Warning!  Warning!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Error!  Error!  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled_widgets.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled_widgets.svg index af81a1c33a..127e3a4dcc 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled_widgets.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_disabled_widgets.svg @@ -19,162 +19,162 @@ font-weight: 700; } - .terminal-1595776013-matrix { + .terminal-1418563853-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1595776013-title { + .terminal-1418563853-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1595776013-r1 { fill: #2d2d2d } -.terminal-1595776013-r2 { fill: #6db2ff } -.terminal-1595776013-r3 { fill: #7ae998 } -.terminal-1595776013-r4 { fill: #ffcf56 } -.terminal-1595776013-r5 { fill: #e76580 } -.terminal-1595776013-r6 { fill: #c5c8c6 } -.terminal-1595776013-r7 { fill: #272727;font-weight: bold } -.terminal-1595776013-r8 { fill: #ddedf9;font-weight: bold } -.terminal-1595776013-r9 { fill: #0a180e;font-weight: bold } -.terminal-1595776013-r10 { fill: #211505;font-weight: bold } -.terminal-1595776013-r11 { fill: #f5e5e9;font-weight: bold } -.terminal-1595776013-r12 { fill: #0d0d0d } -.terminal-1595776013-r13 { fill: #004295 } -.terminal-1595776013-r14 { fill: #008139 } -.terminal-1595776013-r15 { fill: #b86b00 } -.terminal-1595776013-r16 { fill: #780028 } -.terminal-1595776013-r17 { fill: #1e1e1e } -.terminal-1595776013-r18 { fill: #3e6085 } -.terminal-1595776013-r19 { fill: #447b53 } -.terminal-1595776013-r20 { fill: #856e32 } -.terminal-1595776013-r21 { fill: #7a3a47 } -.terminal-1595776013-r22 { fill: #a2a2a2 } -.terminal-1595776013-r23 { fill: #a0a8ae } -.terminal-1595776013-r24 { fill: #0a120c } -.terminal-1595776013-r25 { fill: #150f08 } -.terminal-1595776013-r26 { fill: #aca4a6 } -.terminal-1595776013-r27 { fill: #0f0f0f } -.terminal-1595776013-r28 { fill: #082951 } -.terminal-1595776013-r29 { fill: #084724 } -.terminal-1595776013-r30 { fill: #633d08 } -.terminal-1595776013-r31 { fill: #43081c } -.terminal-1595776013-r32 { fill: #e0e0e0;font-weight: bold } + .terminal-1418563853-r1 { fill: #2d2d2d } +.terminal-1418563853-r2 { fill: #6db2ff } +.terminal-1418563853-r3 { fill: #7ae998 } +.terminal-1418563853-r4 { fill: #ffcf56 } +.terminal-1418563853-r5 { fill: #e76580 } +.terminal-1418563853-r6 { fill: #c5c8c6 } +.terminal-1418563853-r7 { fill: #272727;font-weight: bold } +.terminal-1418563853-r8 { fill: #ddedf9;font-weight: bold } +.terminal-1418563853-r9 { fill: #0a180e;font-weight: bold } +.terminal-1418563853-r10 { fill: #211505;font-weight: bold } +.terminal-1418563853-r11 { fill: #f5e5e9;font-weight: bold } +.terminal-1418563853-r12 { fill: #0d0d0d } +.terminal-1418563853-r13 { fill: #004295 } +.terminal-1418563853-r14 { fill: #008139 } +.terminal-1418563853-r15 { fill: #b86b00 } +.terminal-1418563853-r16 { fill: #780028 } +.terminal-1418563853-r17 { fill: #1e1e1e } +.terminal-1418563853-r18 { fill: #3e6085 } +.terminal-1418563853-r19 { fill: #447b53 } +.terminal-1418563853-r20 { fill: #856e32 } +.terminal-1418563853-r21 { fill: #7a3a47 } +.terminal-1418563853-r22 { fill: #6a6a6a;font-weight: bold } +.terminal-1418563853-r23 { fill: #637f94;font-weight: bold } +.terminal-1418563853-r24 { fill: #193320;font-weight: bold } +.terminal-1418563853-r25 { fill: #422d10;font-weight: bold } +.terminal-1418563853-r26 { fill: #8f7178;font-weight: bold } +.terminal-1418563853-r27 { fill: #0f0f0f } +.terminal-1418563853-r28 { fill: #082951 } +.terminal-1418563853-r29 { fill: #084724 } +.terminal-1418563853-r30 { fill: #633d08 } +.terminal-1418563853-r31 { fill: #43081c } +.terminal-1418563853-r32 { fill: #e0e0e0;font-weight: bold } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + WidgetDisableTestApp - + - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  Button  Button  Button  Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  Button  Button  Button  Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_programmatic_disable_button.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_programmatic_disable_button.svg index 20d0e52bcd..af58d14524 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_programmatic_disable_button.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_programmatic_disable_button.svg @@ -19,137 +19,137 @@ font-weight: 700; } - .terminal-2160157952-matrix { + .terminal-2528666811-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2160157952-title { + .terminal-2528666811-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2160157952-r1 { fill: #e0e0e0 } -.terminal-2160157952-r2 { fill: #c5c8c6 } -.terminal-2160157952-r3 { fill: #1e1e1e } -.terminal-2160157952-r4 { fill: #a2a2a2 } -.terminal-2160157952-r5 { fill: #0f0f0f } -.terminal-2160157952-r6 { fill: #ffa62b;font-weight: bold } -.terminal-2160157952-r7 { fill: #495259 } + .terminal-2528666811-r1 { fill: #e0e0e0 } +.terminal-2528666811-r2 { fill: #c5c8c6 } +.terminal-2528666811-r3 { fill: #1e1e1e } +.terminal-2528666811-r4 { fill: #6a6a6a;font-weight: bold } +.terminal-2528666811-r5 { fill: #0f0f0f } +.terminal-2528666811-r6 { fill: #ffa62b;font-weight: bold } +.terminal-2528666811-r7 { fill: #495259 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ExampleApp + ExampleApp - + - - - - - - - - - - -                        Hover the button then hit space                          -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Disabled  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - space Toggle Button                                                ^p palette + + + + + + + + + + +                        Hover the button then hit space                          +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Disabled  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + space Toggle Button                                                ^p palette diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tabbed_content_with_modified_tabs.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tabbed_content_with_modified_tabs.svg index 6331c437bd..14eb0e779b 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tabbed_content_with_modified_tabs.svg +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tabbed_content_with_modified_tabs.svg @@ -19,139 +19,139 @@ font-weight: 700; } - .terminal-3843185498-matrix { + .terminal-3443677784-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3843185498-title { + .terminal-3443677784-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3843185498-r1 { fill: #c5c8c6 } -.terminal-3843185498-r2 { fill: #ddedf9;font-weight: bold } -.terminal-3843185498-r3 { fill: #454545 } -.terminal-3843185498-r4 { fill: #797979 } -.terminal-3843185498-r5 { fill: #e0e0e0 } -.terminal-3843185498-r6 { fill: #4f4f4f } -.terminal-3843185498-r7 { fill: #0178d4 } -.terminal-3843185498-r8 { fill: #981515 } -.terminal-3843185498-r9 { fill: #e99c9c } -.terminal-3843185498-r10 { fill: #880606 } + .terminal-3443677784-r1 { fill: #c5c8c6 } +.terminal-3443677784-r2 { fill: #ddedf9;font-weight: bold } +.terminal-3443677784-r3 { fill: #454545 } +.terminal-3443677784-r4 { fill: #797979 } +.terminal-3443677784-r5 { fill: #e0e0e0 } +.terminal-3443677784-r6 { fill: #4f4f4f } +.terminal-3443677784-r7 { fill: #0178d4 } +.terminal-3443677784-r8 { fill: #981515 } +.terminal-3443677784-r9 { fill: #c56363;font-weight: bold } +.terminal-3443677784-r10 { fill: #880606 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - FiddleWithTabsApp + FiddleWithTabsApp - + - - Tab 1Tab 2Tab 4Tab 5 -━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - Button  -▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - - - - - - - - - - - - - - - - - - + + Tab 1Tab 2Tab 4Tab 5 +━━━━━╺━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Button  +▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + From 672daf8aa8250a89639036a1a51b05521fdf4de0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 10:16:16 +0000 Subject: [PATCH 16/31] Add text area --- src/textual/demo/widgets.py | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index cb0693cf5a..5df4012fed 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -33,9 +33,11 @@ RadioButton, RadioSet, RichLog, + Select, Sparkline, Switch, TabbedContent, + TextArea, ) WIDGETS_MD = """\ @@ -528,6 +530,60 @@ def switch_theme() -> None: self.set_timer(0.3, switch_theme) +class TextAreas(containers.VerticalGroup): + ALLOW_MAXIMIZE = True + DEFAULT_CLASSES = "column" + TEXTAREA_MD = """\ +## TextArea + +A powerful and highly configurable text area that supports syntax highlighting, line numbers, soft wrapping, and more. + +""" + DEFAULT_CSS = """ + TextAreas { + TextArea { + height: 16; + } + &.-maximized { + height: 1fr; + } + } + """ + DEFAULT_TEXT = """\ +# Start building! +from textual import App, ComposeResult +""" + + def compose(self) -> ComposeResult: + yield Markdown(self.TEXTAREA_MD) + yield Select.from_values( + [ + "Bash", + "Css", + "Go", + "HTML", + "Java", + "Javascript", + "JSON", + "Kotlin", + "Markdown", + "Python", + "Rust", + "Regex", + "Sql", + "TOML", + "YAML", + ], + value="Python", + prompt="Highlight language", + ) + + yield TextArea(self.DEFAULT_TEXT, show_line_numbers=True) + + def on_select_changed(self, event: Select.Changed) -> None: + self.query_one(TextArea).language = (event.value or "").lower() + + class WidgetsScreen(PageScreen): """The Widgets screen""" @@ -561,4 +617,5 @@ def compose(self) -> ComposeResult: yield Logs() yield Sparklines() yield Switches() + yield TextAreas() yield Footer() From df630305d2f395bdbea0fd6572275b881552cef3 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 12:09:39 +0000 Subject: [PATCH 17/31] markdowns --- src/textual/demo/widgets.py | 134 ++++++++++++++++++++++++++++++++++-- src/textual/lazy.py | 94 +++++++++++-------------- src/textual/widget.py | 18 ++++- 3 files changed, 187 insertions(+), 59 deletions(-) diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index 5df4012fed..258e1e2be8 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -128,7 +128,6 @@ class Checkboxes(containers.VerticalGroup): """ RADIOSET_MD = """\ - ### Radio Sets A *radio set* is a list of mutually exclusive options. @@ -408,6 +407,108 @@ def update_rich_log(self) -> None: rich_log.write(traceback, animate=True) +class Markdowns(containers.VerticalGroup): + DEFAULT_CLASSES = "column" + DEFAULT_CSS = """ + Markdowns { + #container { + border: tall transparent; + height: 16; + padding: 0 1; + &:focus { border: tall $border; } + &.-maximized { height: 1fr; } + } + #movies { + padding: 0 1; + MarkdownBlock { padding: 0 1 0 0; } + } + } + """ + MD_MD = """\ +## Markdown + +Display Markdown in your apps with the Markdown widget. +Most of the text on this page is Markdown. + +Here's an AI generated Markdown document: + +""" + MOVIES_MD = """\ +# The Golden Age of Action Cinema: The 1980s + +The 1980s marked a transformative era in action cinema, defined by **excessive machismo**, explosive practical effects, and unforgettable one-liners. This decade gave birth to many of Hollywood's most enduring action franchises, from _Die Hard_ to _Rambo_, setting templates that filmmakers still reference today. + +## Technical Innovation + +Technologically, the 80s represented a sweet spot between practical effects and early CGI. Filmmakers relied heavily on: + +* Practical stunts +* Pyrotechnics +* Hand-built models + +These elements lent the films a tangible quality that many argue remains superior to modern digital effects. + +## The Action Hero Archetype + +The quintessential action hero emerged during this period, with key characteristics: + +1. Impressive physique +2. Military background +3. Anti-authority attitude +4. Memorable catchphrases + +> "I'll be back" - The Terminator (1984) + +Heroes like Arnold Schwarzenegger and Sylvester Stallone became global icons. However, the decade also saw more nuanced characters emerge, like Bruce Willis's everyman John McClane in *Die Hard*, and powerful female protagonists like Sigourney Weaver's Ellen Ripley in *Aliens*. + +### Political Influence + +Cold War politics heavily influenced these films' narratives, with many plots featuring American heroes facing off against Soviet adversaries. This political subtext, combined with themes of individual triumph over bureaucratic systems, perfectly captured the era's zeitgeist. + +--- + +While often dismissed as simple entertainment, 80s action films left an indelible mark on cinema history, influencing everything from filming techniques to narrative structures, and continuing to inspire filmmakers and delight audiences decades later. + +""" + + def compose(self) -> ComposeResult: + yield Markdown(self.MD_MD) + with containers.VerticalScroll( + id="container", can_focus=True, can_maximize=True + ): + yield Markdown(self.MOVIES_MD, id="movies") + + +class Selects(containers.VerticalGroup): + DEFAULT_CLASSES = "column" + SELECTS_MD = """\ +## Selects + +Selects (AKA *Combo boxes*), present a list of options in a menu that may be expanded by the user. +""" + HEROS = [ + "Arnold Schwarzenegger", + "Brigitte Nielsen", + "Bruce Willis", + "Carl Weathers", + "Chuck Norris", + "Dolph Lundgren", + "Grace Jones", + "Harrison Ford", + "Jean-Claude Van Damme", + "Kurt Russell", + "Linda Hamilton", + "Mel Gibson", + "Michelle Yeoh", + "Sigourney Weaver", + "Sylvester Stallone", + ] + + def compose(self) -> ComposeResult: + yield Markdown(self.SELECTS_MD) + yield Select.from_values(self.HEROS, prompt="80s action hero") + + class Sparklines(containers.VerticalGroup): """Demonstrates sparklines.""" @@ -514,6 +615,10 @@ def on_click(self, event: events.Click) -> None: def on_switch_changed(self, event: Switch.Changed) -> None: # Don't issue more Changed events + if not event.value: + self.query_one("#textual-dark", Switch).value = True + return + with self.prevent(Switch.Changed): # Reset all other switches for switch in self.query("Switch").results(Switch): @@ -578,10 +683,28 @@ def compose(self) -> ComposeResult: prompt="Highlight language", ) - yield TextArea(self.DEFAULT_TEXT, show_line_numbers=True) + yield TextArea(self.DEFAULT_TEXT, show_line_numbers=True, language=None) def on_select_changed(self, event: Select.Changed) -> None: - self.query_one(TextArea).language = (event.value or "").lower() + self.query_one(TextArea).language = ( + event.value.lower() if isinstance(event.value, str) else None + ) + + +class YourWidgets(containers.VerticalGroup): + DEFAULT_CLASSES = "column" + YOUR_MD = """\ +## Your widget here + +The Textual API allows you to [build custom re-usable widgets](https://textual.textualize.io/guide/widgets/#custom-widgets) and share them across projects. +Custom widgets can be themed, just like the builtin widget library. + +Combine existing widgets to add new functionality, or use the powerful [Line API](https://textual.textualize.io/guide/widgets/#line-api) for unique creations. + +""" + + def compose(self) -> ComposeResult: + yield Markdown(self.YOUR_MD) class WidgetsScreen(PageScreen): @@ -607,7 +730,7 @@ class WidgetsScreen(PageScreen): BINDINGS = [Binding("escape", "blur", "Unfocus any focused widget", show=False)] def compose(self) -> ComposeResult: - with lazy.Reveal(containers.VerticalScroll(can_focus=False)): + with lazy.Reveal(containers.VerticalScroll(can_focus=True)): yield Markdown(WIDGETS_MD, classes="column") yield Buttons() yield Checkboxes() @@ -615,7 +738,10 @@ def compose(self) -> ComposeResult: yield Inputs() yield ListViews() yield Logs() + yield Markdowns() + yield Selects() yield Sparklines() yield Switches() yield TextAreas() + yield YourWidgets() yield Footer() diff --git a/src/textual/lazy.py b/src/textual/lazy.py index 436025f063..921d9e46db 100644 --- a/src/textual/lazy.py +++ b/src/textual/lazy.py @@ -4,8 +4,6 @@ from __future__ import annotations -from functools import partial - from textual.widget import Widget @@ -66,82 +64,72 @@ async def mount() -> None: class Reveal(Widget): + """Similar to [Lazy][textual.lazy.Lazy], but mounts children sequentially. + + This is useful when you have so many child widgets that there is a noticeable delay before + you see anything. By mounting the children over several frames, the user will feel that + something is happening. + + Example: + ```python + def compose(self) -> ComposeResult: + with lazy.Reveal(containers.VerticalScroll(can_focus=False)): + yield Markdown(WIDGETS_MD, classes="column") + yield Buttons() + yield Checkboxes() + yield Datatables() + yield Inputs() + yield ListViews() + yield Logs() + yield Sparklines() + yield Footer() + ``` + """ + DEFAULT_CSS = """ Reveal { display: none; } """ - def __init__(self, widget: Widget, delay: float = 1 / 60) -> None: - """Similar to [Lazy][textual.lazy.Lazy], but also displays *children* sequentially. - - The first frame will display the first child with all other children hidden. - The remaining children will be displayed 1-by-1, over as may frames are required. - - This is useful when you have so many child widgets that there is a noticeable delay before - you see anything. By mounting the children over several frames, the user will feel that - something is happening. - - Example: - ```python - def compose(self) -> ComposeResult: - with lazy.Reveal(containers.VerticalScroll(can_focus=False)): - yield Markdown(WIDGETS_MD, classes="column") - yield Buttons() - yield Checkboxes() - yield Datatables() - yield Inputs() - yield ListViews() - yield Logs() - yield Sparklines() - yield Footer() - ``` - + def __init__(self, widget: Widget) -> None: + """ Args: - widget: A widget that should be mounted after a refresh. - delay: A (short) delay between mounting widgets. + widget: A widget to mount. """ self._replace_widget = widget - self._delay = delay + self._widgets: list[Widget] = [] super().__init__() @classmethod - def _reveal(cls, parent: Widget, delay: float = 1 / 60) -> None: + def _reveal(cls, parent: Widget, widgets: list[Widget]) -> None: """Reveal children lazily. Args: parent: The parent widget. - delay: A delay between reveals. + widgets: Child widgets. """ - def check_children() -> None: - """Check for un-displayed children.""" - iter_children = iter(parent.children) - for child in iter_children: - if not child.display: - child.display = True - break - for child in iter_children: - if not child.display: - parent.set_timer( - delay, partial(parent.call_after_refresh, check_children) - ) - break - - check_children() + async def check_children() -> None: + """Check for pending children""" + if not widgets: + return + widget = widgets.pop(0) + await parent.mount(widget) + if widgets: + parent.call_next(check_children) + + parent.call_next(check_children) def compose_add_child(self, widget: Widget) -> None: - widget.display = False - self._replace_widget.compose_add_child(widget) + self._widgets.append(widget) async def mount_composed_widgets(self, widgets: list[Widget]) -> None: parent = self.parent if parent is None: return assert isinstance(parent, Widget) - - if self._replace_widget.children: - self._replace_widget.children[0].display = True await parent.mount(self._replace_widget, after=self) await self.remove() - self._reveal(self._replace_widget, self._delay) + self._reveal(self._replace_widget, self._widgets.copy()) + self._widgets.clear() diff --git a/src/textual/widget.py b/src/textual/widget.py index a6fb1697f9..0e6fb8e25b 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -9,6 +9,7 @@ from collections import Counter from contextlib import asynccontextmanager from fractions import Fraction +from time import monotonic from types import TracebackType from typing import ( TYPE_CHECKING, @@ -491,6 +492,8 @@ def __init__( """Used to cache :last-of-type pseudoclass state.""" self._odd: tuple[int, bool] = (-1, False) """Used to cache :odd pseudoclass state.""" + self._last_scroll_time = monotonic() + """Time of last scroll.""" @property def is_mounted(self) -> bool: @@ -2211,6 +2214,7 @@ def is_scrollable(self) -> bool: @property def is_scrolling(self) -> bool: """Is this widget currently scrolling?""" + current_time = monotonic() for node in self.ancestors: if not isinstance(node, Widget): break @@ -2219,6 +2223,9 @@ def is_scrolling(self) -> bool: or node.scroll_y != node.scroll_target_y ): return True + if current_time - node._last_scroll_time < 0.1: + # Scroll ended very recently + return True return False @property @@ -2360,6 +2367,11 @@ def _scroll_to( animator.force_stop_animation(self, "scroll_x") animator.force_stop_animation(self, "scroll_y") + def _animate_on_complete(): + self._last_scroll_time = monotonic() + if on_complete is not None: + self.call_next(on_complete) + if animate: # TODO: configure animation speed if duration is None and speed is None: @@ -2378,7 +2390,7 @@ def _scroll_to( speed=speed, duration=duration, easing=easing, - on_complete=on_complete, + on_complete=_animate_on_complete, level=level, ) scrolled_x = True @@ -2392,7 +2404,7 @@ def _scroll_to( speed=speed, duration=duration, easing=easing, - on_complete=on_complete, + on_complete=_animate_on_complete, level=level, ) scrolled_y = True @@ -2409,6 +2421,7 @@ def _scroll_to( self.scroll_target_y = self.scroll_y = y scrolled_y = scroll_y != self.scroll_y + self._last_scroll_time = monotonic() if on_complete is not None: self.call_after_refresh(on_complete) @@ -2892,6 +2905,7 @@ def scroll_up( force=force, on_complete=on_complete, level=level, + immediate=immediate, ) def _scroll_up_for_pointer( From 09a439035df0dc73b350fb05a6b380bf4a8b5445 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 12:14:37 +0000 Subject: [PATCH 18/31] catch no screen --- src/textual/message_pump.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 58fb76d1c1..2d8454e55a 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -844,6 +844,13 @@ async def on_timer(self, event: events.Timer) -> None: event.prevent_default() event.stop() if event.callback is not None: + try: + self.app.screen + except Exception: + self.log.warning( + f"Not invoking timer callback {event.callback!r} because there is no screen." + ) + return try: await invoke(event.callback) except Exception as error: From 86fc98a008c45c67aeec401b9680311b2b1c942e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 14:15:18 +0000 Subject: [PATCH 19/31] tree demo --- CHANGELOG.md | 1 + src/textual/demo/data.py | 88 ++++++++++++++++++++++++++++++++++++ src/textual/demo/widgets.py | 39 +++++++++++++++- src/textual/widgets/_tree.py | 46 +++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e6ccf7f9..8fe26e232f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added support for in-band terminal resize protocol https://github.com/Textualize/textual/pull/5217 - Added TEXTUAL_THEME environment var, which should be a comma separated list of desired themes - Added `Widget.is_scrolling` +- Added `Tree.add_json` ### Changed diff --git a/src/textual/demo/data.py b/src/textual/demo/data.py index 19b2f7dde1..4469846b3b 100644 --- a/src/textual/demo/data.py +++ b/src/textual/demo/data.py @@ -1,3 +1,5 @@ +import json + COUNTRIES = [ "Afghanistan", "Albania", @@ -325,3 +327,89 @@ 1989-11-17,All Dogs Go to Heaven,Animation,Don Bluth,27,G,84 1989-12-20,Tango & Cash,Action,Andrei Konchalovsky,63,R,104 """ + +MOVIES_JSON = """{ + "decades": { + "1980s": { + "genres": { + "action": { + "franchises": { + "terminator": { + "name": "The Terminator", + "movies": [ + { + "title": "The Terminator", + "year": 1984, + "director": "James Cameron", + "stars": ["Arnold Schwarzenegger", "Linda Hamilton", "Michael Biehn"], + "boxOffice": 78371200, + "quotes": ["I'll be back", "Come with me if you want to live"] + } + ] + }, + "rambo": { + "name": "Rambo", + "movies": [ + { + "title": "First Blood", + "year": 1982, + "director": "Ted Kotcheff", + "stars": ["Sylvester Stallone", "Richard Crenna", "Brian Dennehy"], + "boxOffice": 47212904 + }, + { + "title": "Rambo: First Blood Part II", + "year": 1985, + "director": "George P. Cosmatos", + "stars": ["Sylvester Stallone", "Richard Crenna", "Charles Napier"], + "boxOffice": 150415432 + } + ] + } + }, + "standalone_classics": { + "die_hard": { + "title": "Die Hard", + "year": 1988, + "director": "John McTiernan", + "stars": ["Bruce Willis", "Alan Rickman", "Reginald VelJohnson"], + "boxOffice": 140700000, + "location": "Nakatomi Plaza", + "quotes": ["Yippee-ki-yay, motherf***er"] + }, + "predator": { + "title": "Predator", + "year": 1987, + "director": "John McTiernan", + "stars": ["Arnold Schwarzenegger", "Carl Weathers", "Jesse Ventura"], + "boxOffice": 98267558, + "location": "Val Verde jungle", + "quotes": ["Get to the chopper!"] + } + }, + "common_themes": [ + "Cold War politics", + "One man army", + "Revenge plots", + "Military operations", + "Law enforcement" + ], + "typical_elements": { + "weapons": ["M60 machine gun", "Desert Eagle", "Explosive arrows"], + "vehicles": ["Military helicopters", "Muscle cars", "Tanks"], + "locations": ["Urban jungle", "Actual jungle", "Industrial facilities"] + } + } + } + } + }, + "metadata": { + "total_movies": 4, + "date_compiled": "2024", + "box_office_total": 467654094, + "most_frequent_actor": "Arnold Schwarzenegger", + "most_frequent_director": "John McTiernan" + } +}""" + +MOVIES_TREE = json.loads(MOVIES_JSON) diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index 258e1e2be8..87c1dfb1a8 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -11,7 +11,7 @@ from textual import containers, events, lazy, on from textual.app import ComposeResult from textual.binding import Binding -from textual.demo.data import COUNTRIES, MOVIES +from textual.demo.data import COUNTRIES, MOVIES, MOVIES_TREE from textual.demo.page import PageScreen from textual.reactive import reactive, var from textual.suggester import SuggestFromList @@ -38,6 +38,7 @@ Switch, TabbedContent, TextArea, + Tree, ) WIDGETS_MD = """\ @@ -635,6 +636,41 @@ def switch_theme() -> None: self.set_timer(0.3, switch_theme) +class Trees(containers.VerticalGroup): + DEFAULT_CLASSES = "column" + TREES_MD = """\ +## Tree + +The Tree widget displays hierarchical data. + +There is also the Tree widget's cousin, DirectoryTree, to navigate folders and files on the filesystem. + """ + DEFAULT_CSS = """ + Trees { + Tree { + height: 16; + &.-maximized { height: 1fr; } + } + VerticalGroup { + border: heavy transparent; + &:focus-within { border: heavy $border; } + } + } + + """ + + def compose(self) -> ComposeResult: + yield Markdown(self.TREES_MD) + with containers.VerticalGroup(): + yield Tree("80s movies") + + def on_mount(self) -> None: + tree = self.query_one(Tree) + tree.show_root = False + tree.add_json(MOVIES_TREE) + tree.root.expand() + + class TextAreas(containers.VerticalGroup): ALLOW_MAXIMIZE = True DEFAULT_CLASSES = "column" @@ -743,5 +779,6 @@ def compose(self) -> ComposeResult: yield Sparklines() yield Switches() yield TextAreas() + yield Trees() yield YourWidgets() yield Footer() diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index afda7a707c..79c64be224 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -785,6 +785,52 @@ def __init__( super().__init__(name=name, id=id, classes=classes, disabled=disabled) + def add_json(self, json_data: object, node: TreeNode | None = None) -> None: + """Adds JSON data to a node. + + Args: + json_data: An object decoded from JSON. + node: Node to add data to. + + """ + + if node is None: + node = self.root + + from rich.highlighter import ReprHighlighter + + highlighter = ReprHighlighter() + + def add_node(name: str, node: TreeNode, data: object) -> None: + """Adds a node to the tree. + + Args: + name: Name of the node. + node: Parent node. + data: Data associated with the node. + """ + if isinstance(data, dict): + node.set_label(Text(f"{{}} {name}")) + for key, value in data.items(): + new_node = node.add("") + add_node(key, new_node, value) + elif isinstance(data, list): + node.set_label(Text(f"[] {name}")) + for index, value in enumerate(data): + new_node = node.add("") + add_node(str(index), new_node, value) + else: + node.allow_expand = False + if name: + label = Text.assemble( + Text.from_markup(f"[b]{name}[/b]="), highlighter(repr(data)) + ) + else: + label = Text(repr(data)) + node.set_label(label) + + add_node("", node, json_data) + @property def cursor_node(self) -> TreeNode[TreeDataType] | None: """The currently selected node, or ``None`` if no selection.""" From f74e4db5e07a7fab7ccf6d8cb5e884bfed784195 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 14:28:46 +0000 Subject: [PATCH 20/31] harden lazy reveal --- src/textual/lazy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/textual/lazy.py b/src/textual/lazy.py index 921d9e46db..58ab6a3e2e 100644 --- a/src/textual/lazy.py +++ b/src/textual/lazy.py @@ -115,9 +115,15 @@ async def check_children() -> None: if not widgets: return widget = widgets.pop(0) - await parent.mount(widget) + try: + await parent.mount(widget) + except Exception: + # I think this can occur if the parent is removed before all children are added + # Only noticed this on shutdown + return + if widgets: - parent.call_next(check_children) + parent.set_timer(0.02, check_children) parent.call_next(check_children) From 326e6c4fe31a7225380927405e110c08fc8b4619 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 16:02:33 +0000 Subject: [PATCH 21/31] tabs demo --- src/textual/demo/data.py | 43 +++++++++++++++++++++++++++++++++++++ src/textual/demo/widgets.py | 27 ++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/textual/demo/data.py b/src/textual/demo/data.py index 4469846b3b..01ca66e58d 100644 --- a/src/textual/demo/data.py +++ b/src/textual/demo/data.py @@ -413,3 +413,46 @@ }""" MOVIES_TREE = json.loads(MOVIES_JSON) + +DUNE_BIOS = [ + { + "name": "Paul Atreides", + "description": "Heir to House Atreides who becomes the Fremen messiah Muad'Dib. Born with extraordinary mental abilities due to Bene Gesserit breeding program.", + }, + { + "name": "Lady Jessica", + "description": "Bene Gesserit concubine to Duke Leto and mother of Paul. Defied her order by bearing a son instead of a daughter, disrupting centuries of careful breeding.", + }, + { + "name": "Baron Vladimir Harkonnen", + "description": "Cruel and corpulent leader of House Harkonnen, sworn enemy of House Atreides. Known for his cunning and brutality in pursuing power.", + }, + { + "name": "Leto Atreides", + "description": "Noble Duke and father of Paul, known for his honor and just rule. Accepts governorship of Arrakis despite knowing it's likely a trap.", + }, + { + "name": "Stilgar", + "description": "Leader of the Fremen Sietch Tabr, becomes a loyal supporter of Paul. Skilled warrior who helps train Paul in Fremen ways.", + }, + { + "name": "Chani", + "description": "Fremen warrior and daughter of planetologist Liet-Kynes. Becomes Paul's concubine and true love after appearing in his prescient visions.", + }, + { + "name": "Thufir Hawat", + "description": "Mentat and Master of Assassins for House Atreides. Serves three generations of Atreides with his superhuman computational skills.", + }, + { + "name": "Duncan Idaho", + "description": "Swordmaster of the Ginaz, loyal to House Atreides. Known for his exceptional fighting skills and sacrifice to save Paul and Jessica.", + }, + { + "name": "Gurney Halleck", + "description": "Warrior-troubadour of House Atreides, skilled with sword and baliset. Serves as Paul's weapons teacher and loyal friend.", + }, + { + "name": "Dr. Yueh", + "description": "Suk doctor conditioned against taking human life, but betrays House Atreides after the Harkonnens torture his wife. Imperial Conditioning broken.", + }, +] diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index 87c1dfb1a8..36eba29faa 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -11,7 +11,7 @@ from textual import containers, events, lazy, on from textual.app import ComposeResult from textual.binding import Binding -from textual.demo.data import COUNTRIES, MOVIES, MOVIES_TREE +from textual.demo.data import COUNTRIES, DUNE_BIOS, MOVIES, MOVIES_TREE from textual.demo.page import PageScreen from textual.reactive import reactive, var from textual.suggester import SuggestFromList @@ -35,6 +35,7 @@ RichLog, Select, Sparkline, + Static, Switch, TabbedContent, TextArea, @@ -636,6 +637,29 @@ def switch_theme() -> None: self.set_timer(0.3, switch_theme) +class TabsDemo(containers.VerticalGroup): + DEFAULT_CLASSES = "column" + TABS_MD = """\ +## Tabs + +A navigable list of section headers. + +Typically used with `ContentTabs`, to display additional content associate with each tab. + +Use the cursor keys to navigate. + +""" + DEFAULT_CSS = """ + .bio { padding: 1 2; background: $boost; color: $foreground-muted; } + """ + + def compose(self) -> ComposeResult: + yield Markdown(self.TABS_MD) + with TabbedContent(*[bio["name"] for bio in DUNE_BIOS]): + for bio in DUNE_BIOS: + yield Static(bio["description"], classes="bio") + + class Trees(containers.VerticalGroup): DEFAULT_CLASSES = "column" TREES_MD = """\ @@ -778,6 +802,7 @@ def compose(self) -> ComposeResult: yield Selects() yield Sparklines() yield Switches() + yield TabsDemo() yield TextAreas() yield Trees() yield YourWidgets() From a0d458789df52174c9dea99e95c506996fea3285 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 16:42:06 +0000 Subject: [PATCH 22/31] test fixes --- src/textual/app.py | 5 ----- tests/css/test_nested_css.py | 4 ++-- .../snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py | 1 - tests/snapshot_tests/snapshot_apps/scroll_to.py | 1 - tests/test_lazy.py | 7 ++++--- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 561eceeac5..5889e97f5d 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -462,9 +462,6 @@ class MyApp(App[None]): SUSPENDED_SCREEN_CLASS: ClassVar[str] = "" """Class to apply to suspended screens, or empty string for no class.""" - HOVER_EFFECTS_SCROLL_PAUSE: ClassVar[float] = 0.02 - """Seconds to pause hover effects for when scrolling.""" - _PSEUDO_CLASSES: ClassVar[dict[str, Callable[[App[Any]], bool]]] = { "focus": lambda app: app.app_focus, "blur": lambda app: not app.app_focus, @@ -2824,8 +2821,6 @@ def _set_mouse_over(self, widget: Widget | None) -> None: Args: widget: Widget under mouse, or None for no widgets. """ - if widget is not None and widget.is_scrolling: - return if widget is None: if self.mouse_over is not None: try: diff --git a/tests/css/test_nested_css.py b/tests/css/test_nested_css.py index e73d67e686..bd6b5714a8 100644 --- a/tests/css/test_nested_css.py +++ b/tests/css/test_nested_css.py @@ -72,10 +72,10 @@ async def test_lists_of_selectors_in_nested_css() -> None: class DeclarationAfterNestedApp(App[None]): CSS = """ Screen { + background: green; Label { background: red; - } - background: green; + } } """ diff --git a/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py b/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py index 563cd0aba9..afbbf0966c 100644 --- a/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py +++ b/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py @@ -4,7 +4,6 @@ class ScrollOffByOne(App): AUTO_FOCUS = None - HOVER_EFFECTS_SCROLL_PAUSE = 0.0 def compose(self) -> ComposeResult: for number in range(1, 100): diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to.py b/tests/snapshot_tests/snapshot_apps/scroll_to.py index bded6e5ff4..986af6701f 100644 --- a/tests/snapshot_tests/snapshot_apps/scroll_to.py +++ b/tests/snapshot_tests/snapshot_apps/scroll_to.py @@ -6,7 +6,6 @@ class ScrollOffByOne(App): """Scroll to item 50.""" AUTO_FOCUS = None - HOVER_EFFECTS_SCROLL_PAUSE = 0 def compose(self) -> ComposeResult: for number in range(1, 100): diff --git a/tests/test_lazy.py b/tests/test_lazy.py index 5beb591855..c430782be1 100644 --- a/tests/test_lazy.py +++ b/tests/test_lazy.py @@ -39,10 +39,11 @@ async def test_lazy_reveal(): async with app.run_test() as pilot: # No #foo on initial mount - # Only first child should be visible initially + # Only first child should be available initially assert app.query_one("#foo").display - assert not app.query_one("#bar").display - assert not app.query_one("#baz").display + # Next two aren't mounted yet + assert not app.query("#bar") + assert not app.query("#baz") # All children should be visible after a pause await pilot.pause() From 12f3cc67fa495f0dbe6112d2b381d8e505db1504 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 17:02:23 +0000 Subject: [PATCH 23/31] catch bad screen on timer --- src/textual/demo/demo_app.py | 2 ++ src/textual/message_pump.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/textual/demo/demo_app.py b/src/textual/demo/demo_app.py index a3ec87bf94..3b51c9d4bb 100644 --- a/src/textual/demo/demo_app.py +++ b/src/textual/demo/demo_app.py @@ -65,6 +65,8 @@ class DemoApp(App): ] def action_maximize(self) -> None: + if self.screen.is_maximized: + return if self.screen.focused is None: self.notify( "Nothing to be maximized (try pressing [b]tab[/b])", diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 2d8454e55a..d47c51cf1c 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -836,6 +836,13 @@ def post_message(self, message: Message) -> bool: async def on_callback(self, event: events.Callback) -> None: if self.app._closing: return + try: + self.app.screen + except Exception: + self.log.warning( + f"Not invoking timer callback {event.callback!r} because there is no screen." + ) + return await invoke(event.callback) async def on_timer(self, event: events.Timer) -> None: From 07bd5dbbfd375318f6425789426568b28545d9a7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 15 Nov 2024 18:17:44 +0000 Subject: [PATCH 24/31] timing issue in test --- tests/test_lazy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_lazy.py b/tests/test_lazy.py index c430782be1..e862d0ce1f 100644 --- a/tests/test_lazy.py +++ b/tests/test_lazy.py @@ -42,7 +42,6 @@ async def test_lazy_reveal(): # Only first child should be available initially assert app.query_one("#foo").display # Next two aren't mounted yet - assert not app.query("#bar") assert not app.query("#baz") # All children should be visible after a pause From d795c43dd16c82e3127b56918e2b00af957e2691 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 17:08:50 +0000 Subject: [PATCH 25/31] Refine --- src/textual/demo/demo_app.py | 2 +- src/textual/demo/widgets.py | 25 ++++++++++++------------- src/textual/renderables/sparkline.py | 6 ++++++ src/textual/widgets/_sparkline.py | 2 -- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/textual/demo/demo_app.py b/src/textual/demo/demo_app.py index 3b51c9d4bb..66b4d5837e 100644 --- a/src/textual/demo/demo_app.py +++ b/src/textual/demo/demo_app.py @@ -57,7 +57,7 @@ class DemoApp(App): tooltip="Save an SVG 'screenshot' of the current screen", ), Binding( - "ctrl+m", + "ctrl+a", "app.maximize", "Maximize", tooltip="Maximized the focused widget (if possible)", diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index 36eba29faa..3dfcb56a54 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -537,7 +537,7 @@ class Sparklines(containers.VerticalGroup): VerticalScroll { height: auto; border: heavy transparent; - &:focus { border: heavy $accent; } + &:focus { border: heavy $border; } } } @@ -686,13 +686,11 @@ class Trees(containers.VerticalGroup): def compose(self) -> ComposeResult: yield Markdown(self.TREES_MD) with containers.VerticalGroup(): - yield Tree("80s movies") - - def on_mount(self) -> None: - tree = self.query_one(Tree) - tree.show_root = False - tree.add_json(MOVIES_TREE) - tree.root.expand() + tree = Tree("80s movies") + tree.show_root = False + tree.add_json(MOVIES_TREE) + tree.root.expand() + yield tree class TextAreas(containers.VerticalGroup): @@ -754,7 +752,7 @@ def on_select_changed(self, event: Select.Changed) -> None: class YourWidgets(containers.VerticalGroup): DEFAULT_CLASSES = "column" YOUR_MD = """\ -## Your widget here +## Your Widget Here! The Textual API allows you to [build custom re-usable widgets](https://textual.textualize.io/guide/widgets/#custom-widgets) and share them across projects. Custom widgets can be themed, just like the builtin widget library. @@ -762,6 +760,9 @@ class YourWidgets(containers.VerticalGroup): Combine existing widgets to add new functionality, or use the powerful [Line API](https://textual.textualize.io/guide/widgets/#line-api) for unique creations. """ + DEFAULT_CSS = """ + YourWidgets { margin-bottom: 2; } + """ def compose(self) -> ComposeResult: yield Markdown(self.YOUR_MD) @@ -773,13 +774,11 @@ class WidgetsScreen(PageScreen): CSS = """ WidgetsScreen { align-horizontal: center; - Markdown { - background: transparent; - } + Markdown { background: transparent; } & > VerticalScroll { scrollbar-gutter: stable; &> * { - &:last-of-type { margin-bottom: 2; } + &:even { background: $boost; } padding-bottom: 1; } diff --git a/src/textual/renderables/sparkline.py b/src/textual/renderables/sparkline.py index 4da25fa233..c2c15608d4 100644 --- a/src/textual/renderables/sparkline.py +++ b/src/textual/renderables/sparkline.py @@ -6,6 +6,7 @@ from rich.color import Color from rich.console import Console, ConsoleOptions, RenderResult +from rich.measure import Measurement from rich.segment import Segment from rich.style import Style @@ -95,6 +96,11 @@ def __rich_console__( bucket_index += step yield Segment(self.BARS[bar_index], Style.from_color(bar_color)) + def __rich_measure__( + self, console: "Console", options: "ConsoleOptions" + ) -> Measurement: + return Measurement(self.width or options.max_width, 1) + if __name__ == "__main__": console = Console() diff --git a/src/textual/widgets/_sparkline.py b/src/textual/widgets/_sparkline.py index 0bc040c7a8..aeb55382a1 100644 --- a/src/textual/widgets/_sparkline.py +++ b/src/textual/widgets/_sparkline.py @@ -88,8 +88,6 @@ def __init__( def render(self) -> RenderResult: """Renders the sparkline when there is data available.""" data = self.data or [] - if not data: - return "" _, base = self.background_colors min_color = base + ( self.get_component_styles("sparkline--min-color").color From 57e65d95d0bb7213bc716bc8ab0c9271c0aee44a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 17:42:43 +0000 Subject: [PATCH 26/31] more efficient order styles --- src/textual/css/stylesheet.py | 5 ++++- src/textual/demo/widgets.py | 7 +++---- src/textual/dom.py | 2 ++ src/textual/timer.py | 6 +++++- src/textual/widget.py | 15 +++++++++++---- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index dd321a2fa4..fe3e3fac6e 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -482,7 +482,10 @@ def apply( node._has_hover_style = "hover" in all_pseudo_classes node._has_focus_within = "focus-within" in all_pseudo_classes node._has_order_style = not all_pseudo_classes.isdisjoint( - {"first-of-type", "last-of-type", "odd", "even"} + {"first-of-type", "last-of-type"} + ) + node._has_odd_or_even = ( + "odd" in all_pseudo_classes or "even" in all_pseudo_classes ) cache_key: tuple | None = None diff --git a/src/textual/demo/widgets.py b/src/textual/demo/widgets.py index 3dfcb56a54..d9b17bc01d 100644 --- a/src/textual/demo/widgets.py +++ b/src/textual/demo/widgets.py @@ -775,10 +775,9 @@ class WidgetsScreen(PageScreen): WidgetsScreen { align-horizontal: center; Markdown { background: transparent; } - & > VerticalScroll { + #container { scrollbar-gutter: stable; - &> * { - + & > * { &:even { background: $boost; } padding-bottom: 1; } @@ -789,7 +788,7 @@ class WidgetsScreen(PageScreen): BINDINGS = [Binding("escape", "blur", "Unfocus any focused widget", show=False)] def compose(self) -> ComposeResult: - with lazy.Reveal(containers.VerticalScroll(can_focus=True)): + with lazy.Reveal(containers.VerticalScroll(id="container", can_focus=True)): yield Markdown(WIDGETS_MD, classes="column") yield Buttons() yield Checkboxes() diff --git a/src/textual/dom.py b/src/textual/dom.py index de4e9c8fe3..68212bc8b6 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -223,6 +223,8 @@ def __init__( self._has_focus_within: bool = False self._has_order_style: bool = False """The node has an ordered dependent pseudo-style (`:odd`, `:even`, `:first-of-type`, `:last-of-type`)""" + self._has_odd_or_even: bool = False + """The node has the pseudo class `odd` or `even`.""" self._reactive_connect: ( dict[str, tuple[MessagePump, Reactive[object] | object]] | None ) = None diff --git a/src/textual/timer.py b/src/textual/timer.py index 3a26774b8a..2470453c29 100644 --- a/src/textual/timer.py +++ b/src/textual/timer.py @@ -177,6 +177,11 @@ async def _run(self) -> None: async def _tick(self, *, next_timer: float, count: int) -> None: """Triggers the Timer's action: either call its callback, or sends an event to its target""" + + app = active_app.get() + if app._exit: + return + if self._callback is not None: try: await invoke(self._callback) @@ -185,7 +190,6 @@ async def _tick(self, *, next_timer: float, count: int) -> None: # Re-raise CancelledErrors that would be caught by the following exception block in Python 3.7 raise except Exception as error: - app = active_app.get() app._handle_exception(error) else: event = events.Timer( diff --git a/src/textual/widget.py b/src/textual/widget.py index 0e6fb8e25b..df1fbe1501 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1253,11 +1253,18 @@ def mount( parent, *widgets, before=insert_before, after=insert_after ) - def update_styles(children: Iterable[DOMNode]) -> None: + def update_styles(children: list[DOMNode]) -> None: """Update order related CSS""" - for child in children: - if child._has_order_style: - child._update_styles() + if before is not None or after is not None: + # If the new children aren't at the end. + # we need to update both odd/even and first-of-type/last-of-type + for child in children: + if child._has_order_style or child._has_odd_or_even: + child._update_styles() + else: + for child in children: + if child._has_order_style: + child._update_styles() self.call_later(update_styles, list(self.children)) await_mount = AwaitMount(self, mounted) From 35035628eee3132c1852b3641ea015ef44a74d41 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 17:52:20 +0000 Subject: [PATCH 27/31] Changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fe26e232f..fdab96a9c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,9 +28,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `textual.theme.ThemeProvider`, a command palette provider which returns all registered themes https://github.com/Textualize/textual/pull/5087 - Added several new built-in CSS variables https://github.com/Textualize/textual/pull/5087 - Added support for in-band terminal resize protocol https://github.com/Textualize/textual/pull/5217 -- Added TEXTUAL_THEME environment var, which should be a comma separated list of desired themes -- Added `Widget.is_scrolling` -- Added `Tree.add_json` +- Added TEXTUAL_THEME environment var, which should be a comma separated list of desired themes https://github.com/Textualize/textual/pull/5238 +- Added `Widget.is_scrolling` https://github.com/Textualize/textual/pull/5238 +- Added `Tree.add_json` https://github.com/Textualize/textual/pull/5238 ### Changed @@ -45,7 +45,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Removed -- Removed `App.HOVER_EFFECTS_SCROLL_PAUSE` +- Removed `App.HOVER_EFFECTS_SCROLL_PAUSE` https://github.com/Textualize/textual/pull/5238 ## [0.85.2] - 2024-11-02 From 6f793247034de02d174b359e1c0b205173051efe Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 17:53:08 +0000 Subject: [PATCH 28/31] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdab96a9c4..3d504e5366 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `can_focus` and `can_focus_children` parameters to scrollable container types. https://github.com/Textualize/textual/pull/5226 - Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226 - Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226 -- `Click` events can now be used with the on decorator to match the initially clicked widget +- `Click` events can now be used with the on decorator to match the initially clicked widget https://github.com/Textualize/textual/pull/5238 - Breaking change: Removed `App.dark` reactive attribute https://github.com/Textualize/textual/pull/5087 - Breaking change: To improve consistency, several changes have been made to default widget CSS and the CSS variables which ship with Textual. On upgrading, your app will likely look different. All of these changes can be overidden with your own CSS. https://github.com/Textualize/textual/pull/5087 From d8f149de5cb0ea832ce6748ec8d19466ba8c61db Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 17:56:29 +0000 Subject: [PATCH 29/31] refine --- src/textual/renderables/_blend_colors.py | 2 -- src/textual/widget.py | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/textual/renderables/_blend_colors.py b/src/textual/renderables/_blend_colors.py index c08f4ddc45..24476faf69 100644 --- a/src/textual/renderables/_blend_colors.py +++ b/src/textual/renderables/_blend_colors.py @@ -15,8 +15,6 @@ def blend_colors(color1: Color, color2: Color, ratio: float) -> Color: Returns: A Color representing the blending of the two supplied colors. """ - # assert color1.triplet is not None - # assert color2.triplet is not None if color1.triplet is None or color2.triplet is None: return color2 r1, g1, b1 = color1.triplet diff --git a/src/textual/widget.py b/src/textual/widget.py index df1fbe1501..9f758b64cb 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2374,7 +2374,8 @@ def _scroll_to( animator.force_stop_animation(self, "scroll_x") animator.force_stop_animation(self, "scroll_y") - def _animate_on_complete(): + def _animate_on_complete() -> None: + """set last scroll time, and invoke callback.""" self._last_scroll_time = monotonic() if on_complete is not None: self.call_next(on_complete) From 91c71e564e34d8056de4dc1b13a98964a08c5eab Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 18:00:43 +0000 Subject: [PATCH 30/31] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d504e5366..f48505f1a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `can_focus` and `can_focus_children` parameters to scrollable container types. https://github.com/Textualize/textual/pull/5226 - Added `textual.lazy.Reveal` https://github.com/Textualize/textual/pull/5226 - Added `Screen.action_blur` https://github.com/Textualize/textual/pull/5226 -- `Click` events can now be used with the on decorator to match the initially clicked widget https://github.com/Textualize/textual/pull/5238 +- `Click` events can now be used with the on decorator to match the originally clicked widget https://github.com/Textualize/textual/pull/5238 - Breaking change: Removed `App.dark` reactive attribute https://github.com/Textualize/textual/pull/5087 - Breaking change: To improve consistency, several changes have been made to default widget CSS and the CSS variables which ship with Textual. On upgrading, your app will likely look different. All of these changes can be overidden with your own CSS. https://github.com/Textualize/textual/pull/5087 From 060ae89b34615524347626652982ac9e1a4dba51 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 16 Nov 2024 18:01:32 +0000 Subject: [PATCH 31/31] version bump --- CHANGELOG.md | 3 ++- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f48505f1a1..b2baa2c889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 +## [0.86.0] ### Fixed @@ -2535,6 +2535,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040 - New handler system for messages that doesn't require inheritance - Improved traceback handling +[0.86.0]: https://github.com/Textualize/textual/compare/v0.85.2...v0.86.0 [0.85.2]: https://github.com/Textualize/textual/compare/v0.85.1...v0.85.2 [0.85.1]: https://github.com/Textualize/textual/compare/v0.85.0...v0.85.1 [0.85.0]: https://github.com/Textualize/textual/compare/v0.84.0...v0.85.0 diff --git a/pyproject.toml b/pyproject.toml index 57a2f605e7..daec8095f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "textual" -version = "0.85.2" +version = "0.86.0" homepage = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual" documentation = "https://textual.textualize.io/"