From e6f620db44bba28075a6b89a42030df65555d39b Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Thu, 9 Mar 2023 20:32:27 +0000 Subject: [PATCH 1/6] Remove underscore pauses from (hopefully) all snapshots This seeks to remove the "_" from the key presses of snapshots and inline runs in the documentation. The "_" no longer does anything, but got legacy reasons at the moment we can't actually allow "_" to be an input to a "faked" keypress in the docs and snapshots. Removing these clears the way to letting "_" have the same status as any other character. See #1994. --- docs/getting_started.md | 2 +- docs/guide/CSS.md | 2 +- docs/guide/app.md | 6 +++--- docs/guide/devtools.md | 2 +- docs/guide/input.md | 4 ++-- docs/guide/layout.md | 4 ++-- docs/guide/screens.md | 6 +++--- docs/guide/styles.md | 2 +- docs/guide/widgets.md | 2 +- docs/index.md | 2 +- docs/styles/dock.md | 2 +- docs/tutorial.md | 8 ++++---- docs/widget_gallery.md | 2 +- docs/widgets/text_log.md | 2 +- tests/snapshot_tests/test_snapshots.py | 10 +++++----- 15 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 48ac26378..82739d337 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -44,7 +44,7 @@ python -m textual If Textual is installed you should see the following: -```{.textual path="src/textual/demo.py" columns="127" lines="53" press="enter,_,_,_,_,_,_,tab,_,w,i,l,l"} +```{.textual path="src/textual/demo.py" columns="127" lines="53" press="enter,tab,w,i,l,l"} ``` ## Examples diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 985c4b41f..49d297b8f 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -68,7 +68,7 @@ Let's look at a trivial Textual app. === "Output" - ```{.textual path="docs/examples/guide/dom1.py" press="_"} + ```{.textual path="docs/examples/guide/dom1.py"} ``` This example creates an instance of `ExampleApp`, which will implicitly create a `Screen` object. In DOM terms, the `Screen` is a _child_ of `ExampleApp`. diff --git a/docs/guide/app.md b/docs/guide/app.md index 891ea72a5..382e55aba 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -27,7 +27,7 @@ Apps don't get much simpler than this—don't expect it to do much. If we run this app with `python simple02.py` you will see a blank terminal, something like the following: -```{.textual path="docs/examples/app/simple02.py" press="_"} +```{.textual path="docs/examples/app/simple02.py"} ``` When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*). @@ -57,7 +57,7 @@ Another such event is the *key* event which is sent when the user presses a key. The `on_mount` handler sets the `self.screen.styles.background` attribute to `"darkblue"` which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run this code. -```{.textual path="docs/examples/app/event01.py" hl_lines="23-25" press="_"} +```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"} ``` The key event handler (`on_key`) has an `event` parameter which will receive a [Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list. @@ -114,7 +114,7 @@ Here's an app which adds a welcome widget in response to any key press: When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets. -```{.textual path="docs/examples/app/widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"} +```{.textual path="docs/examples/app/widgets02.py" press="a,a,a,down,down,down,down,down,down"} ``` ## Exiting diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index 8643073ff..fccbce004 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -60,7 +60,7 @@ textual console You should see the Textual devtools welcome message: -```{.textual title="textual console" path="docs/examples/getting_started/console.py", press="_,_"} +```{.textual title="textual console" path="docs/examples/getting_started/console.py"} ``` In the other console, run your application with `textual run` and the `--dev` switch: diff --git a/docs/guide/input.md b/docs/guide/input.md index c92421266..adc7ebea7 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -20,7 +20,7 @@ The most fundamental way to receive input is via [Key][textual.events.Key] event === "Output" - ```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"} + ```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!"} ``` When you press a key, the app will receive the event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens. @@ -102,7 +102,7 @@ The following example shows how focus works in practice. === "Output" - ```{.textual path="docs/examples/guide/input/key03.py", press="tab,H,e,l,l,o,tab,W,o,r,l,d,!,_"} + ```{.textual path="docs/examples/guide/input/key03.py", press="tab,H,e,l,l,o,tab,W,o,r,l,d,!"} ``` The app splits the screen in to quarters, with a `TextLog` widget in each quarter. If you click any of the text logs, you should see that it is highlighted to show that the widget has focus. Key events will be sent to the focused widget only. diff --git a/docs/guide/layout.md b/docs/guide/layout.md index 3fe1184e0..ba142f9ad 100644 --- a/docs/guide/layout.md +++ b/docs/guide/layout.md @@ -444,7 +444,7 @@ The code below shows a simple sidebar implementation. === "Output" - ```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down,_,_,_,_,_"} + ```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down"} ``` === "dock_layout1_sidebar.py" @@ -468,7 +468,7 @@ This new sidebar is double the width of the one previous one, and has a `deeppin === "Output" - ```{.textual path="docs/examples/guide/layout/dock_layout2_sidebar.py" press="pagedown,down,down,_,_,_,_,_"} + ```{.textual path="docs/examples/guide/layout/dock_layout2_sidebar.py" press="pagedown,down,down"} ``` === "dock_layout2_sidebar.py" diff --git a/docs/guide/screens.md b/docs/guide/screens.md index 28a048e25..5ddefe31f 100644 --- a/docs/guide/screens.md +++ b/docs/guide/screens.md @@ -36,7 +36,7 @@ Let's look at a simple example of writing a screen class to simulate Window's [b === "Output" - ```{.textual path="docs/examples/guide/screens/screen01.py" press="b,_"} + ```{.textual path="docs/examples/guide/screens/screen01.py" press="b"} ``` If you run this you will see an empty screen. Hit the ++b++ key to show a blue screen of death. Hit ++escape++ to return to the default screen. @@ -65,7 +65,7 @@ You can also _install_ new named screens dynamically with the [install_screen][t === "Output" - ```{.textual path="docs/examples/guide/screens/screen02.py" press="b,_"} + ```{.textual path="docs/examples/guide/screens/screen02.py" press="b"} ``` Although both do the same thing, we recommend `SCREENS` for screens that exist for the lifetime of your app. @@ -145,7 +145,7 @@ Screens can be used to implement modal dialogs. The following example pushes a s === "Output" - ```{.textual path="docs/examples/guide/screens/modal01.py" press="q,_"} + ```{.textual path="docs/examples/guide/screens/modal01.py" press="q"} ``` Note the `request_quit` action in the app which pushes a new instance of `QuitScreen`. This makes the quit screen active. if you click cancel, the quit screen calls `pop_screen` to return the default screen. This also removes and deletes the `QuitScreen` object. diff --git a/docs/guide/styles.md b/docs/guide/styles.md index bcaa05db6..436228638 100644 --- a/docs/guide/styles.md +++ b/docs/guide/styles.md @@ -20,7 +20,7 @@ The first line sets the [background](../styles/background.md) style to `"darkblu The second line sets [border](../styles/border.md) to a tuple of `("heavy", "white")` which tells Textual to draw a white border with a style of `"heavy"`. Running this code will show the following: -```{.textual path="docs/examples/guide/styles/screen.py" press="_"} +```{.textual path="docs/examples/guide/styles/screen.py"} ``` ## Styling widgets diff --git a/docs/guide/widgets.md b/docs/guide/widgets.md index 78e222894..8dad39114 100644 --- a/docs/guide/widgets.md +++ b/docs/guide/widgets.md @@ -136,7 +136,7 @@ Let's use markup links in the hello example so that the greeting becomes a link === "Output" - ```{.textual path="docs/examples/guide/widgets/hello05.py" press="_"} + ```{.textual path="docs/examples/guide/widgets/hello05.py"} ``` If you run this example you will see that the greeting has been underlined, which indicates it is clickable. If you click on the greeting it will run the `next_word` action which updates the next word. diff --git a/docs/index.md b/docs/index.md index b3ca42e71..c8e48c574 100644 --- a/docs/index.md +++ b/docs/index.md @@ -77,7 +77,7 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t ```{.textual path="examples/pride.py"} ``` -```{.textual path="docs/examples/tutorial/stopwatch.py" columns="100" lines="30" press="d,tab,enter,_,_"} +```{.textual path="docs/examples/tutorial/stopwatch.py" columns="100" lines="30" press="d,tab,enter"} ``` diff --git a/docs/styles/dock.md b/docs/styles/dock.md index 9fbd241ac..28462fd4f 100644 --- a/docs/styles/dock.md +++ b/docs/styles/dock.md @@ -19,7 +19,7 @@ Notice that even though the content is scrolled, the sidebar remains fixed. === "Output" - ```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down,_,_,_,_,_"} + ```{.textual path="docs/examples/guide/layout/dock_layout1_sidebar.py" press="pagedown,down,down"} ``` === "dock_layout1_sidebar.py" diff --git a/docs/tutorial.md b/docs/tutorial.md index 7d37eea58..532557acc 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -25,7 +25,7 @@ This will be a simple yet **fully featured** app — you could distribute th Here's what the finished app will look like: -```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"} +```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,tab,enter,tab,enter,tab,enter"} ``` ### Get the code @@ -339,7 +339,7 @@ The `on_button_pressed` method is an *event handler*. Event handlers are methods If you run `stopwatch04.py` now you will be able to toggle between the two states by clicking the first button: -```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,_,enter,_,_,_"} +```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"} ``` ## Reactive attributes @@ -421,7 +421,7 @@ This code supplies missing features and makes our app useful. We've made the fol If you run `stopwatch06.py` you will be able to use the stopwatches independently. -```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"} +```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,tab,enter,tab"} ``` The only remaining feature of the Stopwatch app left to implement is the ability to add and remove stopwatches. @@ -449,7 +449,7 @@ The `action_remove_stopwatch` function calls [query()][textual.dom.DOMNode.query If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++. -```{.textual path="docs/examples/tutorial/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"} +```{.textual path="docs/examples/tutorial/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,tab"} ``` ## What next? diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index 149804326..cb8d7cdc4 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -182,7 +182,7 @@ Display and update text in a scrolling panel. [TextLog reference](./widgets/text_log.md){ .md-button .md-button--primary } -```{.textual path="docs/examples/widgets/text_log.py" press="_,H,i"} +```{.textual path="docs/examples/widgets/text_log.py" press="H,i"} ``` ## Tree diff --git a/docs/widgets/text_log.md b/docs/widgets/text_log.md index bb491a28a..d354fc24b 100644 --- a/docs/widgets/text_log.md +++ b/docs/widgets/text_log.md @@ -13,7 +13,7 @@ The example below shows an application showing a `TextLog` with different kinds === "Output" - ```{.textual path="docs/examples/widgets/text_log.py" press="_,H,i"} + ```{.textual path="docs/examples/widgets/text_log.py" press="H,i"} ``` === "text_log.py" diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 7d338f1d5..5ea05ccfe 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -125,12 +125,12 @@ def test_header_render(snap_compare): def test_list_view(snap_compare): assert snap_compare( - WIDGET_EXAMPLES_DIR / "list_view.py", press=["tab", "down", "down", "up", "_"] + WIDGET_EXAMPLES_DIR / "list_view.py", press=["tab", "down", "down", "up"] ) def test_textlog_max_lines(snap_compare): - assert snap_compare("snapshot_apps/textlog_max_lines.py", press=[*"abcde", "_"]) + assert snap_compare("snapshot_apps/textlog_max_lines.py", press=[*"abcde"]) def test_fr_units(snap_compare): @@ -203,7 +203,7 @@ def test_order_independence(snap_compare): def test_order_independence_toggle(snap_compare): - assert snap_compare("snapshot_apps/layer_order_independence.py", press="t,_") + assert snap_compare("snapshot_apps/layer_order_independence.py", press="t") def test_columns_height(snap_compare): @@ -218,7 +218,7 @@ def test_offsets(snap_compare): def test_nested_auto_heights(snap_compare): """Test refreshing widget within a auto sized container""" - assert snap_compare("snapshot_apps/nested_auto_heights.py", press=["1", "2", "_"]) + assert snap_compare("snapshot_apps/nested_auto_heights.py", press=["1", "2"]) def test_programmatic_scrollbar_gutter_change(snap_compare): @@ -295,4 +295,4 @@ def test_focus_component_class(snap_compare): def test_line_api_scrollbars(snap_compare): - assert snap_compare(SNAPSHOT_APPS_DIR / "line_api_scrollbars.py", press=["_"]) + assert snap_compare(SNAPSHOT_APPS_DIR / "line_api_scrollbars.py") From 48dbe1244edfd57c814207709bd4cad86084f710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Thu, 9 Mar 2023 20:55:49 +0000 Subject: [PATCH 2/6] Add magic comment. (#2010) Related comment: https://github.com/Textualize/textual/pull/2003\#discussion_r1131081129 Related PR: #2003. --- src/textual/driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/driver.py b/src/textual/driver.py index 0e8ac6412..3aee951ef 100644 --- a/src/textual/driver.py +++ b/src/textual/driver.py @@ -55,6 +55,7 @@ class Driver(ABC): and not event.button and self._last_move_event is not None ): + # Deduplicate self._down_buttons while preserving order. buttons = list(dict.fromkeys(self._down_buttons).keys()) self._down_buttons.clear() move_event = self._last_move_event From d3bdaf8ae5727fb00a73f82f30fb8fef52ef7d9c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 10 Mar 2023 10:06:10 +0000 Subject: [PATCH 3/6] fix refresh on remove (#2008) * fix refresh on remove * changelog * optimization * added snapshot --- CHANGELOG.md | 6 + src/textual/app.py | 5 +- src/textual/css/query.py | 2 +- src/textual/widget.py | 2 +- .../__snapshots__/test_snapshots.ambr | 160 ++++++++++++++++++ .../snapshot_apps/remove_auto.py | 38 +++++ tests/snapshot_tests/test_snapshots.py | 15 +- 7 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/remove_auto.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a750b19..5e74ce64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 + +### Fixed + +- Fixed container not resizing when a widget is removed https://github.com/Textualize/textual/issues/2007 + ## [0.14.0] - 2023-03-09 ### Changed diff --git a/src/textual/app.py b/src/textual/app.py index add4df575..9c1d1606e 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -2175,7 +2175,7 @@ class App(Generic[ReturnType], DOMNode): for child in widget._nodes: push(child) - def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove: + def _remove_nodes(self, widgets: list[Widget], parent: DOMNode) -> AwaitRemove: """Remove nodes from DOM, and return an awaitable that awaits cleanup. Args: @@ -2198,7 +2198,8 @@ class App(Generic[ReturnType], DOMNode): await self._prune_nodes(widgets) finally: finished_event.set() - self.refresh(layout=True) + if parent.styles.auto_dimensions: + parent.refresh(layout=True) removed_widgets = self._detach_from_dom(widgets) diff --git a/src/textual/css/query.py b/src/textual/css/query.py index 428209fa4..73e09c61b 100644 --- a/src/textual/css/query.py +++ b/src/textual/css/query.py @@ -355,7 +355,7 @@ class DOMQuery(Generic[QueryType]): An awaitable object that waits for the widgets to be removed. """ app = active_app.get() - await_remove = app._remove_nodes(list(self)) + await_remove = app._remove_nodes(list(self), self._node) return await_remove def set_styles( diff --git a/src/textual/widget.py b/src/textual/widget.py index a2560830f..b9ffc8ef5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -2548,7 +2548,7 @@ class Widget(DOMNode): An awaitable object that waits for the widget to be removed. """ - await_remove = self.app._remove_nodes([self]) + await_remove = self.app._remove_nodes([self], self.parent) return await_remove def render(self) -> RenderableType: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index b69597054..7629fbc21 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -16885,6 +16885,166 @@ ''' # --- +# name: test_remove_with_auto_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VerticalRemoveApp + + + + + + + + + + VerticalRemoveApp + ────────────────────────────────────────────────────────────────────────────── + ──────────────────── + This is a test label + ──────────────────── + ────────────────────────────────────────────────────────────────────────────── + + + + + + + + + + + + + + + + + +  A  Add  D  Delete  + + + + + ''' +# --- # name: test_screen_switch ''' diff --git a/tests/snapshot_tests/snapshot_apps/remove_auto.py b/tests/snapshot_tests/snapshot_apps/remove_auto.py new file mode 100644 index 000000000..1a0d5dc02 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/remove_auto.py @@ -0,0 +1,38 @@ +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widgets import Header, Footer, Label + + +class VerticalRemoveApp(App[None]): + CSS = """ + Vertical { + border: round green; + height: auto; + } + + Label { + border: round yellow; + background: red; + color: yellow; + } + """ + BINDINGS = [ + ("a", "add", "Add"), + ("d", "del", "Delete"), + ] + + def compose(self) -> ComposeResult: + yield Header() + yield Vertical() + yield Footer() + + def action_add(self) -> None: + self.query_one(Vertical).mount(Label("This is a test label")) + + def action_del(self) -> None: + if self.query_one(Vertical).children: + self.query_one(Vertical).children[-1].remove() + + +if __name__ == "__main__": + VerticalRemoveApp().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 5ea05ccfe..d6ae89998 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -170,9 +170,11 @@ def test_content_switcher_example_initial(snap_compare): def test_content_switcher_example_switch(snap_compare): - assert snap_compare(WIDGET_EXAMPLES_DIR / "content_switcher.py", press=[ - "tab", "tab", "enter", "wait:500" - ], terminal_size=(50, 50)) + assert snap_compare( + WIDGET_EXAMPLES_DIR / "content_switcher.py", + press=["tab", "tab", "enter", "wait:500"], + terminal_size=(50, 50), + ) # --- CSS properties --- @@ -234,6 +236,7 @@ def test_programmatic_scrollbar_gutter_change(snap_compare): # --- CLI Preview Apps --- # For our CLI previews e.g. `textual easing`, `textual colors` etc, we have snapshots + def test_borders_preview(snap_compare): assert snap_compare(CLI_PREVIEWS_DIR / "borders.py", press=["tab", "enter"]) @@ -296,3 +299,9 @@ def test_focus_component_class(snap_compare): def test_line_api_scrollbars(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "line_api_scrollbars.py") + + +def test_remove_with_auto_height(snap_compare): + assert snap_compare( + SNAPSHOT_APPS_DIR / "remove_auto.py", press=["a", "a", "a", "d", "d"] + ) From 198190117dd10123f89994ad72330b0eb3d5dc8e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 11 Mar 2023 08:36:13 +0000 Subject: [PATCH 4/6] Loading indicator (#2018) * loading indicator and tests * docs * snapshot * remove snapshot * remove debug main [skip ci] * changelog [skip ci] * make start time private --- CHANGELOG.md | 4 ++ docs/api/loading_indicator.md | 1 + docs/examples/widgets/loading_indicator.py | 12 +++++ docs/widget_gallery.md | 9 ++++ docs/widgets/loading_indicator.md | 24 +++++++++ mkdocs-nav.yml | 2 + src/textual/color.py | 46 ++++++++++++++++ src/textual/dom.py | 1 + src/textual/message_pump.py | 3 +- src/textual/widgets/__init__.py | 2 + src/textual/widgets/__init__.pyi | 1 + src/textual/widgets/_loading_indicator.py | 63 ++++++++++++++++++++++ tests/test_color.py | 30 ++++++++++- 13 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 docs/api/loading_indicator.md create mode 100644 docs/examples/widgets/loading_indicator.py create mode 100644 docs/widgets/loading_indicator.md create mode 100644 src/textual/widgets/_loading_indicator.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e74ce64f..91045d60b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed container not resizing when a widget is removed https://github.com/Textualize/textual/issues/2007 +### Added + +- Added a LoadingIndicator widget https://github.com/Textualize/textual/pull/2018 + ## [0.14.0] - 2023-03-09 ### Changed diff --git a/docs/api/loading_indicator.md b/docs/api/loading_indicator.md new file mode 100644 index 000000000..14b176233 --- /dev/null +++ b/docs/api/loading_indicator.md @@ -0,0 +1 @@ +::: textual.widgets.LoadingIndicator diff --git a/docs/examples/widgets/loading_indicator.py b/docs/examples/widgets/loading_indicator.py new file mode 100644 index 000000000..a7df94bf2 --- /dev/null +++ b/docs/examples/widgets/loading_indicator.py @@ -0,0 +1,12 @@ +from textual.app import App, ComposeResult +from textual.widgets import LoadingIndicator + + +class LoadingApp(App): + def compose(self) -> ComposeResult: + yield LoadingIndicator() + + +if __name__ == "__main__": + app = LoadingApp() + app.run() diff --git a/docs/widget_gallery.md b/docs/widget_gallery.md index cb8d7cdc4..908fe5a65 100644 --- a/docs/widget_gallery.md +++ b/docs/widget_gallery.md @@ -109,6 +109,15 @@ Display a list of items (items may be other widgets). ```{.textual path="docs/examples/widgets/list_view.py"} ``` +## LoadingIndicator + +Display an animation while data is loading. + +[LoadingIndicator reference](./widgets/loading_indicator.md){ .md-button .md-button--primary } + +```{.textual path="docs/examples/widgets/loading_indicator.py"} +``` + ## MarkdownViewer Display and interact with a Markdown document (adds a table of contents and browser-like navigation to Markdown). diff --git a/docs/widgets/loading_indicator.md b/docs/widgets/loading_indicator.md new file mode 100644 index 000000000..cd3e9ffc4 --- /dev/null +++ b/docs/widgets/loading_indicator.md @@ -0,0 +1,24 @@ +# LoadingIndicator + +Displays pulsating dots to indicate when data is being loaded. + +- [ ] Focusable +- [ ] Container + + +=== "Output" + + ```{.textual path="docs/examples/widgets/loading_indicator.py"} + ``` + +=== "loading_indicator.py" + + ```python + --8<-- "docs/examples/widgets/loading_indicator.py" + ``` + + + +## See Also + +* [LoadingIndicator](../api/loading_indicator.md) code reference diff --git a/mkdocs-nav.yml b/mkdocs-nav.yml index 820d601a7..b24b28201 100644 --- a/mkdocs-nav.yml +++ b/mkdocs-nav.yml @@ -132,6 +132,7 @@ nav: - "widgets/label.md" - "widgets/list_item.md" - "widgets/list_view.md" + - "widgets/loading_indicator.md" - "widgets/markdown_viewer.md" - "widgets/markdown.md" - "widgets/placeholder.md" @@ -163,6 +164,7 @@ nav: - "api/label.md" - "api/list_item.md" - "api/list_view.md" + - "api/loading_indicator.md" - "api/markdown_viewer.md" - "api/markdown.md" - "api/message_pump.md" diff --git a/src/textual/color.py b/src/textual/color.py index a5bde511a..99e3796df 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -551,6 +551,52 @@ class Color(NamedTuple): return (WHITE if white_contrast > black_contrast else BLACK).with_alpha(alpha) +class Gradient: + """Defines a color gradient.""" + + def __init__(self, *stops: tuple[float, Color]) -> None: + """Create a color gradient that blends colors to form a spectrum. + + A gradient is defined by a sequence of "stops" consisting of a float and a color. + The stop indicate the color at that point on a spectrum between 0 and 1. + + Args: + stops: A colors stop. + + Raises: + ValueError: If any stops are missing (must be at least a stop for 0 and 1). + """ + self.stops = sorted(stops) + if len(stops) < 2: + raise ValueError("At least 2 stops required.") + if self.stops[0][0] != 0.0: + raise ValueError("First stop must be 0.") + if self.stops[-1][0] != 1.0: + raise ValueError("Last stop must be 1.") + + def get_color(self, position: float) -> Color: + """Get a color from the gradient at a position between 0 and 1. + + Positions that are between stops will return a blended color. + + + Args: + factor: A number between 0 and 1, where 0 is the first stop, and 1 is the last. + + Returns: + A color. + """ + # TODO: consider caching + position = clamp(position, 0.0, 1.0) + for (stop1, color1), (stop2, color2) in zip(self.stops, self.stops[1:]): + if stop2 >= position >= stop1: + return color1.blend( + color2, + (position - stop1) / (stop2 - stop1), + ) + return self.stops[-1][1] + + # Color constants WHITE = Color(255, 255, 255) BLACK = Color(0, 0, 0) diff --git a/src/textual/dom.py b/src/textual/dom.py index 0a32f7f16..457131030 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -158,6 +158,7 @@ class DOMNode(MessagePump): @property def auto_refresh(self) -> float | None: + """Interval to refresh widget, or `None` for no automatic refresh.""" return self._auto_refresh @auto_refresh.setter diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index fd80a6874..3a1ac9eb2 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -526,7 +526,8 @@ class MessagePump(metaclass=MessagePumpMeta): await self.on_event(message) else: await self._on_message(message) - await self._flush_next_callbacks() + if self._next_callbacks: + await self._flush_next_callbacks() def _get_dispatch_methods( self, method_name: str, message: Message diff --git a/src/textual/widgets/__init__.py b/src/textual/widgets/__init__.py index 5dc515dea..154d9d26d 100644 --- a/src/textual/widgets/__init__.py +++ b/src/textual/widgets/__init__.py @@ -21,6 +21,7 @@ if typing.TYPE_CHECKING: from ._label import Label from ._list_item import ListItem from ._list_view import ListView + from ._loading_indicator import LoadingIndicator from ._markdown import Markdown, MarkdownViewer from ._placeholder import Placeholder from ._pretty import Pretty @@ -45,6 +46,7 @@ __all__ = [ "Label", "ListItem", "ListView", + "LoadingIndicator", "Markdown", "MarkdownViewer", "Placeholder", diff --git a/src/textual/widgets/__init__.pyi b/src/textual/widgets/__init__.pyi index 5fe292f2d..2beb8a808 100644 --- a/src/textual/widgets/__init__.pyi +++ b/src/textual/widgets/__init__.pyi @@ -10,6 +10,7 @@ from ._input import Input as Input from ._label import Label as Label from ._list_item import ListItem as ListItem from ._list_view import ListView as ListView +from ._loading_indicator import LoadingIndicator as LoadingIndicator from ._markdown import Markdown as Markdown from ._markdown import MarkdownViewer as MarkdownViewer from ._placeholder import Placeholder as Placeholder diff --git a/src/textual/widgets/_loading_indicator.py b/src/textual/widgets/_loading_indicator.py new file mode 100644 index 000000000..15cdf8ffc --- /dev/null +++ b/src/textual/widgets/_loading_indicator.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from time import time + +from rich.console import RenderableType +from rich.style import Style +from rich.text import Text + +from ..color import Gradient +from ..widget import Widget + + +class LoadingIndicator(Widget): + """Display an animated loading indicator.""" + + COMPONENT_CLASSES = {"loading-indicator--dot"} + + DEFAULT_CSS = """ + LoadingIndicator { + width: 100%; + height: 100%; + content-align: center middle; + } + + LoadingIndicator > .loading-indicator--dot { + color: $accent; + } + + """ + + def on_mount(self) -> None: + self._start_time = time() + self.auto_refresh = 1 / 16 + + def render(self) -> RenderableType: + elapsed = time() - self._start_time + speed = 0.8 + dot = "\u25CF" + dot_styles = self.get_component_styles("loading-indicator--dot") + + base_style = self.rich_style + background = self.background_colors[-1] + color = dot_styles.color + + gradient = Gradient( + (0.0, background.blend(color, 0.1)), + (0.7, color), + (1.0, color.lighten(0.1)), + ) + + blends = [(elapsed * speed - dot_number / 8) % 1 for dot_number in range(5)] + + dots = [ + ( + f"{dot} ", + base_style + + Style.from_color(gradient.get_color((1 - blend) ** 2).rich_color), + ) + for blend in blends + ] + indicator = Text.assemble(*dots) + indicator.rstrip() + return indicator diff --git a/tests/test_color.py b/tests/test_color.py index c4ef8311c..03c34bd19 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -2,7 +2,7 @@ import pytest from rich.color import Color as RichColor from rich.text import Text -from textual.color import Color, Lab, lab_to_rgb, rgb_to_lab +from textual.color import Color, Gradient, Lab, lab_to_rgb, rgb_to_lab def test_rich_color(): @@ -215,3 +215,31 @@ def test_rgb_lab_rgb_roundtrip(): def test_inverse(): assert Color(55, 0, 255, 0.1).inverse == Color(200, 255, 0, 0.1) + + +def test_gradient_errors(): + with pytest.raises(ValueError): + Gradient() + with pytest.raises(ValueError): + Gradient((0, Color.parse("red"))) + + with pytest.raises(ValueError): + Gradient( + (0, Color.parse("red")), + (0.8, Color.parse("blue")), + ) + + +def test_gradient(): + gradient = Gradient( + (0, Color(255, 0, 0)), + (0.5, Color(0, 0, 255)), + (1, Color(0, 255, 0)), + ) + + assert gradient.get_color(-1) == Color(255, 0, 0) + assert gradient.get_color(0) == Color(255, 0, 0) + assert gradient.get_color(1) == Color(0, 255, 0) + assert gradient.get_color(1.2) == Color(0, 255, 0) + assert gradient.get_color(0.5) == Color(0, 0, 255) + assert gradient.get_color(0.7) == Color(0, 101, 153) From fcda3c33504660790e7c14561ab368edb53902c0 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sun, 12 Mar 2023 14:30:15 -0400 Subject: [PATCH 5/6] Properly indicate dependency is only required for development (#2025) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3291312e..b16764bf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ typing-extensions = "^4.4.0" aiohttp = { version = ">=3.8.1", optional = true } click = {version = ">=8.1.2", optional = true} msgpack = { version = ">=1.0.3", optional = true } -mkdocs-exclude = "^1.0.2" +mkdocs-exclude = { version = "^1.0.2", optional = true } [tool.poetry.extras] dev = ["aiohttp", "click", "msgpack"] From 9c5e0336f8504cc692eb7565a07fa54be2fcb56f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 13 Mar 2023 10:39:14 +0000 Subject: [PATCH 6/6] Fix scrollbar (#2024) * Fix scrollbar * changelog PR * fix snapshots --- CHANGELOG.md | 1 + docs/examples/styles/scrollbar_size.py | 4 +- src/textual/widget.py | 4 +- .../__snapshots__/test_snapshots.ambr | 400 ++++++++++++++---- .../snapshot_apps/auto-table.py | 189 +++++++++ .../snapshot_apps/disable_widgets.py | 1 - tests/snapshot_tests/test_snapshots.py | 4 + 7 files changed, 511 insertions(+), 92 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/auto-table.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 91045d60b..0e7bd4878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed container not resizing when a widget is removed https://github.com/Textualize/textual/issues/2007 +- Fixes issue where the horizontal scrollbar would be incorrectly enabled https://github.com/Textualize/textual/pull/2024 ### Added diff --git a/docs/examples/styles/scrollbar_size.py b/docs/examples/styles/scrollbar_size.py index 1bbcec572..bf4390c31 100644 --- a/docs/examples/styles/scrollbar_size.py +++ b/docs/examples/styles/scrollbar_size.py @@ -1,5 +1,5 @@ from textual.app import App -from textual.containers import Vertical +from textual.containers import Container from textual.widgets import Label TEXT = """I must not fear. @@ -14,7 +14,7 @@ Where the fear has gone there will be nothing. Only I will remain. class ScrollbarApp(App): def compose(self): - yield Vertical(Label(TEXT * 5), classes="panel") + yield Container(Label(TEXT * 5), classes="panel") app = ScrollbarApp(css_path="scrollbar_size.css") diff --git a/src/textual/widget.py b/src/textual/widget.py index b9ffc8ef5..5cc76f767 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1010,11 +1010,11 @@ class Widget(DOMNode): show_vertical = self.virtual_size.height > height # When a single scrollbar is shown, the other dimension changes, so we need to recalculate. - if show_vertical and not show_horizontal: + if overflow_x == "auto" and show_vertical and not show_horizontal: show_horizontal = self.virtual_size.width > ( width - styles.scrollbar_size_vertical ) - if show_horizontal and not show_vertical: + if overflow_y == "auto" and show_horizontal and not show_vertical: show_vertical = self.virtual_size.height > ( height - styles.scrollbar_size_horizontal ) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 7629fbc21..cb7b92d30 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -1,3 +1,228 @@ +# name: test_auto_table + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + MyApp + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + oktest + ╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍ +  0 ────────────────────────────────────── 1 ────────────────────────────────────── 2 ───── + +  Foo       Bar         Baz               Foo       Bar         Baz               Foo      +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY▁▁ ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH +  ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH  0123456789  IJKLMNOPQRSTUVWXY ABCDEFGH + ───────────────────────────────────────────────────────────────────────────────────────────── + + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + + + ''' +# --- # name: test_auto_width_input ''' @@ -12103,163 +12328,164 @@ font-weight: 700; } - .terminal-3822425727-matrix { + .terminal-717671184-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-3822425727-title { + .terminal-717671184-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-3822425727-r1 { fill: #c5c8c6 } - .terminal-3822425727-r2 { fill: #e3e3e3 } - .terminal-3822425727-r3 { fill: #313437 } - .terminal-3822425727-r4 { fill: #324f70 } - .terminal-3822425727-r5 { fill: #4f9262 } - .terminal-3822425727-r6 { fill: #a4823a } - .terminal-3822425727-r7 { fill: #904354 } - .terminal-3822425727-r8 { fill: #7c7d7e;font-weight: bold } - .terminal-3822425727-r9 { fill: #75828b;font-weight: bold } - .terminal-3822425727-r10 { fill: #192e1f;font-weight: bold } - .terminal-3822425727-r11 { fill: #3a2a13;font-weight: bold } - .terminal-3822425727-r12 { fill: #978186;font-weight: bold } - .terminal-3822425727-r13 { fill: #101011 } - .terminal-3822425727-r14 { fill: #0c1e39 } - .terminal-3822425727-r15 { fill: #156034 } - .terminal-3822425727-r16 { fill: #825210 } - .terminal-3822425727-r17 { fill: #5b132a } - .terminal-3822425727-r18 { fill: #768189 } - .terminal-3822425727-r19 { fill: #e1e1e1 } - .terminal-3822425727-r20 { fill: #3a2a13 } - .terminal-3822425727-r21 { fill: #7b7b7b } - .terminal-3822425727-r22 { fill: #78838b } - .terminal-3822425727-r23 { fill: #7f8081 } - .terminal-3822425727-r24 { fill: #7c7d7e } - .terminal-3822425727-r25 { fill: #31220c;font-weight: bold } - .terminal-3822425727-r26 { fill: #e2e3e3 } - .terminal-3822425727-r27 { fill: #104e2d } - .terminal-3822425727-r28 { fill: #7a7b7b } - .terminal-3822425727-r29 { fill: #1c1c1c } - .terminal-3822425727-r30 { fill: #191919 } - .terminal-3822425727-r31 { fill: #181818 } - .terminal-3822425727-r32 { fill: #7c7c7c } - .terminal-3822425727-r33 { fill: #494949 } - .terminal-3822425727-r34 { fill: #ddedf9 } + .terminal-717671184-r1 { fill: #c5c8c6 } + .terminal-717671184-r2 { fill: #e3e3e3 } + .terminal-717671184-r3 { fill: #313437 } + .terminal-717671184-r4 { fill: #324f70 } + .terminal-717671184-r5 { fill: #4f9262 } + .terminal-717671184-r6 { fill: #a4823a } + .terminal-717671184-r7 { fill: #904354 } + .terminal-717671184-r8 { fill: #e1e1e1 } + .terminal-717671184-r9 { fill: #7c7d7e;font-weight: bold } + .terminal-717671184-r10 { fill: #75828b;font-weight: bold } + .terminal-717671184-r11 { fill: #192e1f;font-weight: bold } + .terminal-717671184-r12 { fill: #3a2a13;font-weight: bold } + .terminal-717671184-r13 { fill: #978186;font-weight: bold } + .terminal-717671184-r14 { fill: #101011 } + .terminal-717671184-r15 { fill: #0c1e39 } + .terminal-717671184-r16 { fill: #156034 } + .terminal-717671184-r17 { fill: #825210 } + .terminal-717671184-r18 { fill: #5b132a } + .terminal-717671184-r19 { fill: #768189 } + .terminal-717671184-r20 { fill: #3a2a13 } + .terminal-717671184-r21 { fill: #7b7b7b } + .terminal-717671184-r22 { fill: #78838b } + .terminal-717671184-r23 { fill: #7f8081 } + .terminal-717671184-r24 { fill: #7c7d7e } + .terminal-717671184-r25 { fill: #31220c;font-weight: bold } + .terminal-717671184-r26 { fill: #e2e3e3 } + .terminal-717671184-r27 { fill: #104e2d } + .terminal-717671184-r28 { fill: #7a7b7b } + .terminal-717671184-r29 { fill: #1c1c1c } + .terminal-717671184-r30 { fill: #191919 } + .terminal-717671184-r31 { fill: #181818 } + .terminal-717671184-r32 { fill: #7c7c7c } + .terminal-717671184-r33 { fill: #494949 } + .terminal-717671184-r34 { fill: #14191f } + .terminal-717671184-r35 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - WidgetDisableTestApp + WidgetDisableTestApp - - - - WidgetDisableTestApp - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - ButtonButtonButtonButtonButton - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ -  Column 1  Column 2  Column 3  Column 4  -  0         0         0         0         - This is list item 0 - This is list item 1 - ▼ This is a test tree - ├── Leaf 0 - Hello, World! - - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is an empty input with a placeholder - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ - This is some text in an input - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ - + + + + WidgetDisableTestApp + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ButtonButtonButtonButtonButton + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ +  Column 1  Column 2  Column 3  Column 4  +  0         0         0         0         + This is list item 0 + This is list item 1 + ▼ This is a test tree + ├── Leaf 0 + Hello, World! + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is an empty input with a placeholder + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + This is some text in an input + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▇▇ diff --git a/tests/snapshot_tests/snapshot_apps/auto-table.py b/tests/snapshot_tests/snapshot_apps/auto-table.py new file mode 100644 index 000000000..49a6c1842 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/auto-table.py @@ -0,0 +1,189 @@ +from os import urandom +from random import randrange + +from textual.app import App +from textual.containers import Container, Horizontal, Vertical +from textual.screen import Screen +from textual.widgets import DataTable, Header, Label + + +class LabeledBox(Container): + DEFAULT_CSS = """ + LabeledBox { + layers: base_ top_; + width: 100%; + height: 100%; + } + + LabeledBox > Container { + layer: base_; + border: round $primary; + width: 100%; + height: 100%; + layout: vertical; + } + + LabeledBox > Label { + layer: top_; + offset-x: 2; + } + """ + + def __init__(self, title, *args, **kwargs): + self.__label = Label(title) + + super().__init__(self.__label, Container(*args, **kwargs)) + + @property + def label(self): + return self.__label + + +class StatusTable(DataTable): + def __init__(self): + super().__init__() + + self.cursor_type = "row" + self.show_cursor = False + self.add_column("Foo") + self.add_column("Bar") + self.add_column("Baz") + + for _ in range(50): + self.add_row( + "ABCDEFGH", + "0123456789", + "IJKLMNOPQRSTUVWXYZ", + ) + + +class Status(LabeledBox): + DEFAULT_CSS = """ + Status { + width: auto; + } + + Status Container { + width: auto; + } + + Status StatusTable { + width: auto; + margin-top: 1; + scrollbar-gutter: stable; + overflow-x: hidden; + } + """ + + def __init__(self, name: str): + self.__name = name + self.__table = StatusTable() + + super().__init__(f" {self.__name} ", self.__table) + + @property + def name(self) -> str: + return self.__name + + @property + def table(self) -> StatusTable: + return self.__table + + +class Rendering(LabeledBox): + DEFAULT_CSS = """ + #issue-info { + height: auto; + border-bottom: dashed #632CA6; + } + + #statuses-box { + height: 1fr; + width: auto; + } + """ + + def __init__(self): + self.__info = Label("test") + + super().__init__( + "", + Container( + Horizontal(self.__info, id="issue-info"), + Horizontal(*[Status(str(i)) for i in range(4)], id="statuses-box"), + id="issues-box", + ), + ) + + @property + def info(self) -> Label: + return self.__info + + +class Sidebar(LabeledBox): + DEFAULT_CSS = """ + #sidebar-status { + height: auto; + border-bottom: dashed #632CA6; + } + + #sidebar-options { + height: 1fr; + } + """ + + def __init__(self): + self.__status = Label("ok") + self.__options = Vertical() + + super().__init__( + "", + Container(self.__status, id="sidebar-status"), + Container(self.__options, id="sidebar-options"), + ) + + @property + def status(self) -> Label: + return self.__status + + @property + def options(self) -> Vertical: + return self.__options + + +class MyScreen(Screen): + DEFAULT_CSS = """ + #main-content { + layout: grid; + grid-size: 2; + grid-columns: 1fr 5fr; + grid-rows: 1fr; + } + + #main-content-sidebar { + height: 100%; + } + + #main-content-rendering { + height: 100%; + } + """ + + def compose(self): + yield Header() + yield Container( + Container(Sidebar(), id="main-content-sidebar"), + Container(Rendering(), id="main-content-rendering"), + id="main-content", + ) + + +class MyApp(App): + async def on_mount(self): + self.install_screen(MyScreen(), "myscreen") + await self.push_screen("myscreen") + + +if __name__ == "__main__": + app = MyApp() + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/disable_widgets.py b/tests/snapshot_tests/snapshot_apps/disable_widgets.py index 7a241e914..8965fbdac 100644 --- a/tests/snapshot_tests/snapshot_apps/disable_widgets.py +++ b/tests/snapshot_tests/snapshot_apps/disable_widgets.py @@ -17,7 +17,6 @@ from textual.widgets import ( class WidgetDisableTestApp(App[None]): - CSS = """ Horizontal { height: auto; diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index d6ae89998..0b26c83c9 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -305,3 +305,7 @@ def test_remove_with_auto_height(snap_compare): assert snap_compare( SNAPSHOT_APPS_DIR / "remove_auto.py", press=["a", "a", "a", "d", "d"] ) + + +def test_auto_table(snap_compare): + assert snap_compare(SNAPSHOT_APPS_DIR / "auto-table.py", terminal_size=(120, 40))