From 41003e356c34039bb40efcf5c9eca3bc5c03ec08 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 2 Mar 2023 14:39:31 +0000 Subject: [PATCH] Fix content width (#1910) * fix calculation for scrollbars * added snapshot * fix for name change * snapshot * fix for textual colors * remove logs * scrollbar logic * scroll logic * remove dead code * snapshot tests * scrollbar mechanism * tidy * demo tweak * preset window size * no need for repaint * Restore repaint * wait for idle on pause * colors tweak * remove wait for idle * snapshot * small sleep * change stabilizer * debug tweaks * remove debug * remove debug * snapshot test * docstring * changelog * add pause --- CHANGELOG.md | 5 +- src/textual/_layout.py | 22 +- src/textual/app.py | 2 +- src/textual/box_model.py | 1 + src/textual/cli/previews/colors.css | 2 +- src/textual/constants.py | 1 - src/textual/demo.css | 1 + src/textual/message_pump.py | 1 + src/textual/scroll_view.py | 3 +- src/textual/scrollbar.py | 2 +- src/textual/widget.py | 93 ++-- src/textual/widgets/_text_log.py | 2 +- .../__snapshots__/test_snapshots.ambr | 441 ++++++++++++------ .../snapshot_apps/line_api_scrollbars.py | 54 +++ tests/snapshot_tests/test_snapshots.py | 4 + tests/tree/test_tree_messages.py | 5 + 16 files changed, 437 insertions(+), 202 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 76b63dc91..e48eb1545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,10 +21,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Scrolling with cursor keys now moves just one cell https://github.com/Textualize/textual/issues/1897 - -### Fixed - - Fix exceptions in watch methods being hidden on startup https://github.com/Textualize/textual/issues/1886 +- Fixed scrollbar size miscalculation https://github.com/Textualize/textual/pull/1910 +- Fixed slow exit on some terminals https://github.com/Textualize/textual/issues/1920 ## [0.12.1] - 2023-02-25 diff --git a/src/textual/_layout.py b/src/textual/_layout.py index f7ab16312..0cdfe5956 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -25,6 +25,7 @@ class DockArrangeResult: """Shared spacing around the widgets.""" _spatial_map: SpatialMap[WidgetPlacement] | None = None + """A Spatial map to query widget placements.""" @property def spatial_map(self) -> SpatialMap[WidgetPlacement]: @@ -111,14 +112,8 @@ class Layout(ABC): width = 0 else: # Use a size of 0, 0 to ignore relative sizes, since those are flexible anyway - placements = widget._arrange(Size(0, 0)).placements - width = max( - [ - placement.region.right + placement.margin.right - for placement in placements - ], - default=0, - ) + arrangement = widget._arrange(Size(0, 0)) + return arrangement.total_region.right + arrangement.spacing.right return width def get_content_height( @@ -139,13 +134,6 @@ class Layout(ABC): height = 0 else: # Use a height of zero to ignore relative heights - placements = widget._arrange(Size(width, 0)).placements - height = max( - [ - placement.region.bottom + placement.margin.bottom - for placement in placements - ], - default=0, - ) - + arrangement = widget._arrange(Size(width, 0)) + height = arrangement.total_region.bottom + arrangement.spacing.bottom return height diff --git a/src/textual/app.py b/src/textual/app.py index fa70671b0..3ca169e85 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1817,7 +1817,7 @@ class App(Generic[ReturnType], DOMNode): await child._close_messages() async def _shutdown(self) -> None: - self._begin_update() # Prevents any layout / repaint while shutting down + self._begin_batch() # Prevents any layout / repaint while shutting down driver = self._driver self._running = False if driver is not None: diff --git a/src/textual/box_model.py b/src/textual/box_model.py index d43c9b3ff..85f57d9ca 100644 --- a/src/textual/box_model.py +++ b/src/textual/box_model.py @@ -125,6 +125,7 @@ def get_box_model( content_height = min(content_height, max_height) content_height = max(Fraction(0), content_height) + model = BoxModel( content_width + gutter.width, content_height + gutter.height, margin ) diff --git a/src/textual/cli/previews/colors.css b/src/textual/cli/previews/colors.css index caa950430..38a6ce045 100644 --- a/src/textual/cli/previews/colors.css +++ b/src/textual/cli/previews/colors.css @@ -9,7 +9,7 @@ ColorButtons { } ColorButtons > Button { - width: 30; + width: 100%; } ColorsView { diff --git a/src/textual/constants.py b/src/textual/constants.py index 41bbb7ca1..2e57942a7 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -3,7 +3,6 @@ Constants that we might want to expose via the public API. """ - import os from typing_extensions import Final diff --git a/src/textual/demo.css b/src/textual/demo.css index 9941d8ba5..ec352ea21 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -102,6 +102,7 @@ Column { height: auto; min-height: 100vh; align: center top; + overflow: hidden; } diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 448df5323..4697da2c6 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -340,6 +340,7 @@ class MessagePump(metaclass=MessagePumpMeta): """ # We send the InvokeLater message to ourselves first, to ensure we've cleared # out anything already pending in our own queue. + message = messages.InvokeLater(self, partial(callback, *args, **kwargs)) self.post_message_no_wait(message) diff --git a/src/textual/scroll_view.py b/src/textual/scroll_view.py index 1f834e839..dcf6b17cf 100644 --- a/src/textual/scroll_view.py +++ b/src/textual/scroll_view.py @@ -82,7 +82,7 @@ class ScrollView(Widget): Returns: True if anything changed, or False if nothing changed. """ - if self._size != size or container_size != container_size: + if self._size != size or self._container_size != container_size: self.refresh() if ( self._size != size @@ -93,7 +93,6 @@ class ScrollView(Widget): virtual_size = self.virtual_size self._container_size = size - self.styles.gutter.totals self._scroll_update(virtual_size) - self.scroll_to(self.scroll_x, self.scroll_y, animate=False) return True else: return False diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py index 3e9acf913..46a65e27b 100644 --- a/src/textual/scrollbar.py +++ b/src/textual/scrollbar.py @@ -197,7 +197,7 @@ class ScrollBarRender: class ScrollBar(Widget): renderer: ClassVar[Type[ScrollBarRender]] = ScrollBarRender """The class used for rendering scrollbars. - This can be overriden and set to a ScrollBarRender-derived class + This can be overridden and set to a ScrollBarRender-derived class in order to delegate all scrollbar rendering to that class. E.g.: ``` diff --git a/src/textual/widget.py b/src/textual/widget.py index 3dd5f35d8..ea1a70c3c 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -274,7 +274,8 @@ class Widget(DOMNode): self._styles_cache = StylesCache() self._rich_style_cache: dict[str, tuple[Style, Style]] = {} - self._stabilized_scrollbar_size: Size | None = None + self._stabilize_scrollbar: tuple[Size, str, str] | None = None + """Used to prevent scrollbar logic getting stuck in an infinite loop.""" self._lock = Lock() super().__init__( @@ -520,6 +521,7 @@ class Widget(DOMNode): def _clear_arrangement_cache(self) -> None: """Clear arrangement cache, forcing a new arrange operation.""" self._arrangement_cache.clear() + self._stabilize_scrollbar = None def _get_virtual_dom(self) -> Iterable[Widget]: """Get widgets not part of the DOM. @@ -855,14 +857,11 @@ class Widget(DOMNode): """ if self.is_container: assert self._layout is not None - height = ( - self._layout.get_content_height( - self, - container, - viewport, - width, - ) - + self.scrollbar_size_horizontal + height = self._layout.get_content_height( + self, + container, + viewport, + width, ) else: cache_key = width @@ -913,8 +912,7 @@ class Widget(DOMNode): return max( 0, self.virtual_size.width - - self.container_size.width - + self.scrollbar_size_vertical, + - (self.container_size.width - self.scrollbar_size_vertical), ) @property @@ -923,8 +921,7 @@ class Widget(DOMNode): return max( 0, self.virtual_size.height - - self.container_size.height - + self.scrollbar_size_horizontal, + - (self.container_size.height - self.scrollbar_size_horizontal), ) @property @@ -985,9 +982,18 @@ class Widget(DOMNode): styles = self.styles overflow_x = styles.overflow_x overflow_y = styles.overflow_y - width, height = self.container_size - show_horizontal = self.show_horizontal_scrollbar + stabilize_scrollbar = ( + self.container_size, + overflow_x, + overflow_y, + ) + if self._stabilize_scrollbar == stabilize_scrollbar: + return + + width, height = self._container_size + + show_horizontal = False if overflow_x == "hidden": show_horizontal = False elif overflow_x == "scroll": @@ -995,7 +1001,7 @@ class Widget(DOMNode): elif overflow_x == "auto": show_horizontal = self.virtual_size.width > width - show_vertical = self.show_vertical_scrollbar + show_vertical = False if overflow_y == "hidden": show_vertical = False elif overflow_y == "scroll": @@ -1003,16 +1009,17 @@ class Widget(DOMNode): elif overflow_y == "auto": show_vertical = self.virtual_size.height > height - if ( - overflow_x == "auto" - and show_vertical - and not show_horizontal - and self._stabilized_scrollbar_size != self.container_size - ): - show_horizontal = ( - self.virtual_size.width + styles.scrollbar_size_vertical > width + # When a single scrollbar is shown, the other dimension changes, so we need to recalculate. + if show_vertical and not show_horizontal: + show_horizontal = self.virtual_size.width > ( + width - styles.scrollbar_size_vertical ) - self._stabilized_scrollbar_size = self.container_size + if show_horizontal and not show_vertical: + show_vertical = self.virtual_size.height > ( + height - styles.scrollbar_size_horizontal + ) + + self._stabilize_scrollbar = stabilize_scrollbar self.show_horizontal_scrollbar = show_horizontal self.show_vertical_scrollbar = show_vertical @@ -1454,6 +1461,7 @@ class Widget(DOMNode): Returns: `True` if the scroll position changed, otherwise `False`. """ + maybe_scroll_x = x is not None and (self.allow_horizontal_scroll or force) maybe_scroll_y = y is not None and (self.allow_vertical_scroll or force) scrolled_x = scrolled_y = False @@ -2231,7 +2239,7 @@ class Widget(DOMNode): if show_horizontal_scrollbar and show_vertical_scrollbar: ( - _, + window_region, vertical_scrollbar_region, horizontal_scrollbar_region, scrollbar_corner_gap, @@ -2242,18 +2250,34 @@ class Widget(DOMNode): if scrollbar_corner_gap: yield self.scrollbar_corner, scrollbar_corner_gap if vertical_scrollbar_region: - yield self.vertical_scrollbar, vertical_scrollbar_region + scrollbar = self.vertical_scrollbar + scrollbar.window_virtual_size = self.virtual_size.height + scrollbar.window_size = window_region.height + yield scrollbar, vertical_scrollbar_region if horizontal_scrollbar_region: - yield self.horizontal_scrollbar, horizontal_scrollbar_region + scrollbar = self.horizontal_scrollbar + scrollbar.window_virtual_size = self.virtual_size.width + scrollbar.window_size = window_region.width + yield scrollbar, horizontal_scrollbar_region elif show_vertical_scrollbar: - _, scrollbar_region = region.split_vertical(-scrollbar_size_vertical) + window_region, scrollbar_region = region.split_vertical( + -scrollbar_size_vertical + ) if scrollbar_region: - yield self.vertical_scrollbar, scrollbar_region + scrollbar = self.vertical_scrollbar + scrollbar.window_virtual_size = self.virtual_size.height + scrollbar.window_size = window_region.height + yield scrollbar, scrollbar_region elif show_horizontal_scrollbar: - _, scrollbar_region = region.split_horizontal(-scrollbar_size_horizontal) + window_region, scrollbar_region = region.split_horizontal( + -scrollbar_size_horizontal + ) if scrollbar_region: - yield self.horizontal_scrollbar, scrollbar_region + scrollbar = self.horizontal_scrollbar + scrollbar.window_virtual_size = self.virtual_size.width + scrollbar.window_size = window_region.width + yield scrollbar, scrollbar_region def get_pseudo_classes(self) -> Iterable[str]: """Pseudo classes for a widget. @@ -2374,9 +2398,13 @@ class Widget(DOMNode): self.vertical_scrollbar.window_size = ( height - self.scrollbar_size_horizontal ) + if self.vertical_scrollbar._repaint_required: + self.call_next(self.vertical_scrollbar.refresh) if self.show_horizontal_scrollbar: self.horizontal_scrollbar.window_virtual_size = virtual_size.width self.horizontal_scrollbar.window_size = width - self.scrollbar_size_vertical + if self.horizontal_scrollbar._repaint_required: + self.call_next(self.horizontal_scrollbar.refresh) self.scroll_x = self.validate_scroll_x(self.scroll_x) self.scroll_y = self.validate_scroll_y(self.scroll_y) @@ -2498,6 +2526,7 @@ class Widget(DOMNode): """ if layout and not self._layout_required: self._layout_required = True + self._stabilize_scrollbar = None for ancestor in self.ancestors: if not isinstance(ancestor, Widget): break diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index 8416c6f76..fa12226ea 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -152,7 +152,7 @@ class TextLog(ScrollView, can_focus=True): self.refresh() self.lines = self.lines[-self.max_lines :] self.virtual_size = Size(self.max_width, len(self.lines)) - self.scroll_end(animate=False, speed=100) + self.scroll_end(animate=False) def clear(self) -> None: """Clear the text log.""" diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 6fd15c510..744398edd 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -8109,132 +8109,132 @@ font-weight: 700; } - .terminal-849091649-matrix { + .terminal-3799145088-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-849091649-title { + .terminal-3799145088-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-849091649-r1 { fill: #c5c8c6 } - .terminal-849091649-r2 { fill: #3333ff } - .terminal-849091649-r3 { fill: #14191f } + .terminal-3799145088-r1 { fill: #c5c8c6 } + .terminal-3799145088-r2 { fill: #3333ff } + .terminal-3799145088-r3 { fill: #14191f } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - ScrollbarApp + ScrollbarApp - - - - - - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me.▂▂▂▂ - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. - I must not fear. - Fear is the mind-killer. - Fear is the little-death that brings total obliteration. - I will face my fear. - I will permit it to pass over me and through me. - And when it has gone past, I will turn the inner eye to see its path. - Where the fear has gone there will be nothing. Only I will remain. + + + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration.▁▁▁▁ + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + + + + + + + + + + @@ -11162,163 +11162,162 @@ font-weight: 700; } - .terminal-1429392340-matrix { + .terminal-52139843-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1429392340-title { + .terminal-52139843-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1429392340-r1 { fill: #c5c8c6 } - .terminal-1429392340-r2 { fill: #e3e3e3 } - .terminal-1429392340-r3 { fill: #313437 } - .terminal-1429392340-r4 { fill: #324f70 } - .terminal-1429392340-r5 { fill: #4f9262 } - .terminal-1429392340-r6 { fill: #a4823a } - .terminal-1429392340-r7 { fill: #904354 } - .terminal-1429392340-r8 { fill: #e1e1e1 } - .terminal-1429392340-r9 { fill: #7c7d7e;font-weight: bold } - .terminal-1429392340-r10 { fill: #75828b;font-weight: bold } - .terminal-1429392340-r11 { fill: #192e1f;font-weight: bold } - .terminal-1429392340-r12 { fill: #3a2a13;font-weight: bold } - .terminal-1429392340-r13 { fill: #978186;font-weight: bold } - .terminal-1429392340-r14 { fill: #101011 } - .terminal-1429392340-r15 { fill: #0c1e39 } - .terminal-1429392340-r16 { fill: #156034 } - .terminal-1429392340-r17 { fill: #825210 } - .terminal-1429392340-r18 { fill: #5b132a } - .terminal-1429392340-r19 { fill: #7b7b7b } - .terminal-1429392340-r20 { fill: #3a2a13 } - .terminal-1429392340-r21 { fill: #78838b } - .terminal-1429392340-r22 { fill: #7f8081 } - .terminal-1429392340-r23 { fill: #7c7d7e } - .terminal-1429392340-r24 { fill: #31220c;font-weight: bold } - .terminal-1429392340-r25 { fill: #e2e3e3 } - .terminal-1429392340-r26 { fill: #104e2d } - .terminal-1429392340-r27 { fill: #7a7b7b } - .terminal-1429392340-r28 { fill: #1c1c1c } - .terminal-1429392340-r29 { fill: #191919 } - .terminal-1429392340-r30 { fill: #181818 } - .terminal-1429392340-r31 { fill: #7c7c7c } - .terminal-1429392340-r32 { fill: #494949 } - .terminal-1429392340-r33 { fill: #14191f } - .terminal-1429392340-r34 { fill: #ddedf9 } + .terminal-52139843-r1 { fill: #c5c8c6 } + .terminal-52139843-r2 { fill: #e3e3e3 } + .terminal-52139843-r3 { fill: #313437 } + .terminal-52139843-r4 { fill: #324f70 } + .terminal-52139843-r5 { fill: #4f9262 } + .terminal-52139843-r6 { fill: #a4823a } + .terminal-52139843-r7 { fill: #904354 } + .terminal-52139843-r8 { fill: #7c7d7e;font-weight: bold } + .terminal-52139843-r9 { fill: #75828b;font-weight: bold } + .terminal-52139843-r10 { fill: #192e1f;font-weight: bold } + .terminal-52139843-r11 { fill: #3a2a13;font-weight: bold } + .terminal-52139843-r12 { fill: #978186;font-weight: bold } + .terminal-52139843-r13 { fill: #101011 } + .terminal-52139843-r14 { fill: #0c1e39 } + .terminal-52139843-r15 { fill: #156034 } + .terminal-52139843-r16 { fill: #825210 } + .terminal-52139843-r17 { fill: #5b132a } + .terminal-52139843-r18 { fill: #7b7b7b } + .terminal-52139843-r19 { fill: #e1e1e1 } + .terminal-52139843-r20 { fill: #3a2a13 } + .terminal-52139843-r21 { fill: #78838b } + .terminal-52139843-r22 { fill: #7f8081 } + .terminal-52139843-r23 { fill: #7c7d7e } + .terminal-52139843-r24 { fill: #31220c;font-weight: bold } + .terminal-52139843-r25 { fill: #e2e3e3 } + .terminal-52139843-r26 { fill: #104e2d } + .terminal-52139843-r27 { fill: #7a7b7b } + .terminal-52139843-r28 { fill: #1c1c1c } + .terminal-52139843-r29 { fill: #191919 } + .terminal-52139843-r30 { fill: #181818 } + .terminal-52139843-r31 { fill: #7c7c7c } + .terminal-52139843-r32 { fill: #494949 } + .terminal-52139843-r33 { 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 + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + @@ -13524,6 +13523,162 @@ ''' # --- +# name: test_line_api_scrollbars + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollViewApp + + + + + + + + + + + + 11 01234567 + 12 01234567 + 13 01234567 + 14 01234567 + 15 01234567▁▁ + 16 01234567 + 17 01234567 + 18 01234567 + 19 01234567 + + 11 01234567 + 12 01234567 + 13 01234567 + 14 01234567 + 15 01234567▁▁ + 16 01234567 + 17 01234567 + 18 01234567 + 19 01234567 + + + + + + + + ''' +# --- # name: test_list_view ''' diff --git a/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py b/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py new file mode 100644 index 000000000..68a16a099 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py @@ -0,0 +1,54 @@ +from rich.text import Text + +from textual.app import App, ComposeResult +from textual.containers import Vertical +from textual.widget import Widget +from textual.widgets import TextLog + + +class MyWidget(Widget): + def render(self): + return Text( + "\n".join(f"{n} 0123456789" for n in range(20)), + no_wrap=True, + overflow="hidden", + justify="left", + ) + + +class ScrollViewApp(App): + CSS = """ + Screen { + align: center middle; + } + + TextLog { + width:13; + height:10; + } + + Vertical{ + width:13; + height: 10; + overflow: scroll; + overflow-x: auto; + } + MyWidget { + width:13; + height:auto; + } + + """ + + def compose(self) -> ComposeResult: + yield TextLog() + yield Vertical(MyWidget()) + + def on_ready(self) -> None: + self.query_one(TextLog).write("\n".join(f"{n} 0123456789" for n in range(20))) + self.query_one(Vertical).scroll_end(animate=False) + + +if __name__ == "__main__": + app = ScrollViewApp() + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index ddf815725..cae6b0c53 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -256,3 +256,7 @@ def test_disabled_widgets(snap_compare): def test_focus_component_class(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "focus_component_class.py", press=["tab"]) + + +def test_line_api_scrollbars(snap_compare): + assert snap_compare(SNAPSHOT_APPS_DIR / "line_api_scrollbars.py", press=["_"]) diff --git a/tests/tree/test_tree_messages.py b/tests/tree/test_tree_messages.py index 6296126e8..359dd1aa0 100644 --- a/tests/tree/test_tree_messages.py +++ b/tests/tree/test_tree_messages.py @@ -46,6 +46,7 @@ async def test_tree_node_selected_message() -> None: """Selecting a node should result in a selected message being emitted.""" async with TreeApp().run_test() as pilot: await pilot.press("enter") + await pilot.pause() assert pilot.app.messages == ["NodeExpanded", "NodeSelected"] @@ -54,6 +55,7 @@ async def test_tree_node_selected_message_no_auto() -> None: async with TreeApp().run_test() as pilot: pilot.app.query_one(MyTree).auto_expand = False await pilot.press("enter") + await pilot.pause() assert pilot.app.messages == ["NodeSelected"] @@ -61,6 +63,7 @@ async def test_tree_node_expanded_message() -> None: """Expanding a node should result in an expanded message being emitted.""" async with TreeApp().run_test() as pilot: await pilot.press("space") + await pilot.pause() assert pilot.app.messages == ["NodeExpanded"] @@ -68,6 +71,7 @@ async def test_tree_node_collapsed_message() -> None: """Collapsing a node should result in a collapsed message being emitted.""" async with TreeApp().run_test() as pilot: await pilot.press("space", "space") + await pilot.pause() assert pilot.app.messages == ["NodeExpanded", "NodeCollapsed"] @@ -75,4 +79,5 @@ async def test_tree_node_highlighted_message() -> None: """Highlighting a node should result in a highlighted message being emitted.""" async with TreeApp().run_test() as pilot: await pilot.press("enter", "down") + await pilot.pause() assert pilot.app.messages == ["NodeExpanded", "NodeSelected", "NodeHighlighted"]