From abb7705ed0463e00d403d0fad7b659aa46e995fc Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 21:06:09 +0100 Subject: [PATCH 1/5] wait for screen (#2584) * wait for screen * comments and changelog * wait for screen after keys * extra wait for animation * comment * comment * docstring --- CHANGELOG.md | 1 + src/textual/message_pump.py | 18 ++++++++++++---- src/textual/pilot.py | 42 ++++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce29efda..145e578a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521 - Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743 - Calling `dismiss` on a screen that is not at the top of the stack now raises an exception https://github.com/Textualize/textual/issues/2575 +- `MessagePump.call_after_refresh` and `MessagePump.call_later` will not return `False` if the callback could not be scheduled. https://github.com/Textualize/textual/pull/2584 ### Fixed diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7439919ce..a4dfc8256 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -349,20 +349,25 @@ class MessagePump(metaclass=_MessagePumpMeta): self._timers.add(timer) return timer - def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + def call_after_refresh(self, callback: Callable, *args: Any, **kwargs: Any) -> bool: """Schedule a callback to run after all messages are processed and the screen has been refreshed. Positional and keyword arguments are passed to the callable. Args: callback: A callable. + + Returns: + `True` if the callback was scheduled, or `False` if the callback could not be + scheduled (may occur if the message pump was closed or closing). + """ # We send the InvokeLater message to ourselves first, to ensure we've cleared # out anything already pending in our own queue. message = messages.InvokeLater(partial(callback, *args, **kwargs)) - self.post_message(message) + return self.post_message(message) - def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> None: + def call_later(self, callback: Callable, *args: Any, **kwargs: Any) -> bool: """Schedule a callback to run after all messages are processed in this object. Positional and keywords arguments are passed to the callable. @@ -370,9 +375,14 @@ class MessagePump(metaclass=_MessagePumpMeta): callback: Callable to call next. *args: Positional arguments to pass to the callable. **kwargs: Keyword arguments to pass to the callable. + + Returns: + `True` if the callback was scheduled, or `False` if the callback could not be + scheduled (may occur if the message pump was closed or closing). + """ message = events.Callback(callback=partial(callback, *args, **kwargs)) - self.post_message(message) + return self.post_message(message) def call_next(self, callback: Callable, *args: Any, **kwargs: Any) -> None: """Schedule a callback to run immediately after processing the current message. diff --git a/src/textual/pilot.py b/src/textual/pilot.py index eaab42334..041e00e13 100644 --- a/src/textual/pilot.py +++ b/src/textual/pilot.py @@ -65,6 +65,7 @@ class Pilot(Generic[ReturnType]): """ if keys: await self._app._press_keys(keys) + await self._wait_for_screen() async def click( self, @@ -132,13 +133,49 @@ class Pilot(Generic[ReturnType]): app.post_message(MouseMove(**message_arguments)) await self.pause() + async def _wait_for_screen(self, timeout: float = 30.0) -> bool: + """Wait for the current screen to have processed all pending events. + + Args: + timeout: A timeout in seconds to wait. + + Returns: + `True` if all events were processed, or `False` if the wait timed out. + """ + children = [self.app, *self.app.screen.walk_children(with_self=True)] + count = 0 + count_zero_event = asyncio.Event() + + def decrement_counter() -> None: + """Decrement internal counter, and set an event if it reaches zero.""" + nonlocal count + count -= 1 + if count == 0: + # When count is zero, all messages queued at the start of the method have been processed + count_zero_event.set() + + # Increase the count for every successful call_later + for child in children: + if child.call_later(decrement_counter): + count += 1 + + if count: + # Wait for the count to return to zero, or a timeout + try: + await asyncio.wait_for(count_zero_event.wait(), timeout=timeout) + except asyncio.TimeoutError: + return False + + return True + async def pause(self, delay: float | None = None) -> None: """Insert a pause. Args: delay: Seconds to pause, or None to wait for cpu idle. """ - # These sleep zeros, are to force asyncio to give up a time-slice, + # These sleep zeros, are to force asyncio to give up a time-slice. + await self._wait_for_screen() if delay is None: await wait_for_idle(0) else: @@ -152,7 +189,9 @@ class Pilot(Generic[ReturnType]): async def wait_for_scheduled_animations(self) -> None: """Wait for any current and scheduled animations to complete.""" + await self._wait_for_screen() await self._app.animator.wait_until_complete() + await self._wait_for_screen() await wait_for_idle() self.app.screen._on_timer_update() @@ -162,5 +201,6 @@ class Pilot(Generic[ReturnType]): Args: result: The app result returned by `run` or `run_async`. """ + await self._wait_for_screen() await wait_for_idle() self.app.exit(result) From 3a17a762334d9a45106321ef5dfdc43ffb848ed1 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 21:34:34 +0100 Subject: [PATCH 2/5] Exit debug (#2554) * show single error by default * changelog * show numbers of errors * changelog --- CHANGELOG.md | 1 + src/textual/app.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 145e578a2..cd3e91dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521 +- Only a single error will be written by default, unless in dev mode ("debug" in App.features) https://github.com/Textualize/textual/issues/2480 - Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743 - Calling `dismiss` on a screen that is not at the top of the stack now raises an exception https://github.com/Textualize/textual/issues/2575 - `MessagePump.call_after_refresh` and `MessagePump.call_later` will not return `False` if the callback could not be scheduled. https://github.com/Textualize/textual/pull/2584 diff --git a/src/textual/app.py b/src/textual/app.py index 841d854e2..c8863d764 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1630,8 +1630,22 @@ class App(Generic[ReturnType], DOMNode): def _print_error_renderables(self) -> None: """Print and clear exit renderables.""" - for renderable in self._exit_renderables: - self.error_console.print(renderable) + error_count = len(self._exit_renderables) + if "debug" in self.features: + for renderable in self._exit_renderables: + self.error_console.print(renderable) + if error_count > 1: + self.error_console.print( + f"\n[b]NOTE:[/b] {error_count} errors show above.", markup=True + ) + elif self._exit_renderables: + self.error_console.print(self._exit_renderables[0]) + if error_count > 1: + self.error_console.print( + f"\n[b]NOTE:[/b] 1 of {error_count} errors show. Run with [b]--dev[/] to see all errors.", + markup=True, + ) + self._exit_renderables.clear() async def _process_messages( From 53e765f7d62a31de288842a65451c7d3efa43cef Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 May 2023 21:34:59 +0100 Subject: [PATCH 3/5] Avoid docks when scrolling (#2571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * handle docked layers * handle scroll better * snapshot update * remove commented out code * superflous * dock gutter * snapshit * snapshit test * changelog * mistake * docstrings * changelog * whitespace * missing punctuation * ofx docstring * Apply suggestions from code review Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --------- Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- CHANGELOG.md | 2 + src/textual/_compositor.py | 21 +- src/textual/_layout.py | 3 +- src/textual/_spatial_map.py | 4 +- src/textual/geometry.py | 10 +- src/textual/screen.py | 2 + src/textual/widget.py | 22 +- .../__snapshots__/test_snapshots.ambr | 491 ++++++++++++++++++ .../snapshot_apps/dock_scroll2.py | 33 ++ .../snapshot_apps/dock_scroll_off_by_one.py | 17 + .../snapshot_apps/horizontal_auto_width.css | 4 +- .../snapshot_tests/snapshot_apps/scroll_to.py | 19 + tests/snapshot_tests/test_snapshots.py | 19 + 13 files changed, 624 insertions(+), 23 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/dock_scroll2.py create mode 100644 tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py create mode 100644 tests/snapshot_tests/snapshot_apps/scroll_to.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3e91dda..990180d91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,10 +24,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 - `footer--description` component class was being ignored https://github.com/Textualize/textual/issues/2544 - Pasting empty selection in `Input` would raise an exception https://github.com/Textualize/textual/issues/2563 +- Fix issue with scrolling and docks https://github.com/Textualize/textual/issues/2525 ### Added - Class variable `AUTO_FOCUS` to screens https://github.com/Textualize/textual/issues/2457 +- Added `NULL_SPACING` and `NULL_REGION` to geometry.py ## [0.24.1] - 2023-05-08 diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 61e4f3de8..300b52029 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -33,7 +33,7 @@ from . import errors from ._cells import cell_len from ._context import visible_screen_stack from ._loop import loop_last -from .geometry import NULL_OFFSET, Offset, Region, Size +from .geometry import NULL_OFFSET, NULL_SPACING, Offset, Region, Size, Spacing from .strip import Strip, StripRenderable if TYPE_CHECKING: @@ -71,6 +71,8 @@ class MapGeometry(NamedTuple): """The container [size][textual.geometry.Size] (area not occupied by scrollbars).""" virtual_region: Region """The [region][textual.geometry.Region] relative to the container (but not necessarily visible).""" + dock_gutter: Spacing + """Space from the container reserved by docked widgets.""" @property def visible_region(self) -> Region: @@ -484,7 +486,7 @@ class Compositor: # Widgets and regions in render order visible_widgets = [ (order, widget, region, clip) - for widget, (region, order, clip, _, _, _) in map.items() + for widget, (region, order, clip, _, _, _, _) in map.items() if in_screen(region) and overlaps(clip, region) ] visible_widgets.sort(key=itemgetter(0), reverse=True) @@ -522,6 +524,7 @@ class Compositor: layer_order: int, clip: Region, visible: bool, + dock_gutter: Spacing, _MapGeometry: type[MapGeometry] = MapGeometry, ) -> None: """Called recursively to place a widget and its children in the map. @@ -591,10 +594,8 @@ class Compositor: get_layer_index = layers_to_index.get - scroll_spacing = arrange_result.scroll_spacing - # Add all the widgets - for sub_region, margin, sub_widget, z, fixed, overlay in reversed( + for sub_region, _, sub_widget, z, fixed, overlay in reversed( placements ): layer_index = get_layer_index(sub_widget.layer, 0) @@ -602,11 +603,6 @@ class Compositor: if fixed: widget_region = sub_region + placement_offset else: - total_region = total_region.union( - sub_region.grow( - margin if layer_index else margin + scroll_spacing - ) - ) widget_region = sub_region + placement_scroll_offset widget_order = order + ((layer_index, z, layer_order),) @@ -629,6 +625,7 @@ class Compositor: layer_order, no_clip if overlay else sub_clip, visible, + arrange_result.scroll_spacing, ) layer_order -= 1 @@ -646,6 +643,7 @@ class Compositor: container_size, container_size, chrome_region, + dock_gutter, ) map[widget] = _MapGeometry( @@ -655,6 +653,7 @@ class Compositor: total_region.size, container_size, virtual_region, + dock_gutter, ) elif visible: @@ -666,6 +665,7 @@ class Compositor: region.size, container_size, virtual_region, + dock_gutter, ) # Add top level (root) widget @@ -677,6 +677,7 @@ class Compositor: layer_order, size.region, True, + NULL_SPACING, ) return map, widgets diff --git a/src/textual/_layout.py b/src/textual/_layout.py index 338b72192..575dc547f 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -51,7 +51,8 @@ class DockArrangeResult: Returns: A Region. """ - return self.spatial_map.total_region + _top, right, bottom, _left = self.scroll_spacing + return self.spatial_map.total_region.grow((0, right, bottom, 0)) def get_visible_placements(self, region: Region) -> list[WidgetPlacement]: """Get the placements visible within the given region. diff --git a/src/textual/_spatial_map.py b/src/textual/_spatial_map.py index af38065dd..3f778a06b 100644 --- a/src/textual/_spatial_map.py +++ b/src/textual/_spatial_map.py @@ -72,11 +72,11 @@ class SpatialMap(Generic[ValueType]): _region_to_grid = self._region_to_grid_coordinates total_region = self.total_region for region, fixed, overlay, value in regions_and_values: - if not overlay: - total_region = total_region.union(region) if fixed: append_fixed(value) else: + if not overlay: + total_region = total_region.union(region) for grid in _region_to_grid(region): get_grid_list(grid).append(value) self.total_region = total_region diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 4b337a4d9..41ca459f6 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -907,7 +907,7 @@ class Region(NamedTuple): class Spacing(NamedTuple): - """The spacing around a renderable, such as padding and border + """The spacing around a renderable, such as padding and border. Spacing is defined by four integers for the space at the top, right, bottom, and left of a region. @@ -940,7 +940,7 @@ class Spacing(NamedTuple): top: int = 0 """Space from the top of a region.""" right: int = 0 - """Space from the left of a region.""" + """Space from the right of a region.""" bottom: int = 0 """Space from the bottom of a region.""" left: int = 0 @@ -1095,3 +1095,9 @@ class Spacing(NamedTuple): NULL_OFFSET: Final = Offset(0, 0) """An [offset][textual.geometry.Offset] constant for (0, 0).""" + +NULL_REGION: Final = Region(0, 0, 0, 0) +"""A [Region][textual.geometry.Region] constant for a null region (at the origin, with both width and height set to zero).""" + +NULL_SPACING: Final = Spacing(0, 0, 0, 0) +"""A [Spacing][textual.geometry.Spacing] constant for no space.""" diff --git a/src/textual/screen.py b/src/textual/screen.py index 535b621ea..775223a03 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -584,6 +584,7 @@ class Screen(Generic[ScreenResultType], Widget): virtual_size, container_size, _, + _, ) in layers: if widget in exposed_widgets: if widget._size_updated( @@ -614,6 +615,7 @@ class Screen(Generic[ScreenResultType], Widget): virtual_size, container_size, _, + _, ) in layers: widget._size_updated(region.size, virtual_size, container_size) if widget in send_resize: diff --git a/src/textual/widget.py b/src/textual/widget.py index b116f5393..6d14cad74 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -57,7 +57,7 @@ from .box_model import BoxModel from .css.query import NoMatches, WrongType from .css.scalar import ScalarOffset from .dom import DOMNode, NoScreen -from .geometry import Offset, Region, Size, Spacing, clamp +from .geometry import NULL_REGION, NULL_SPACING, Offset, Region, Size, Spacing, clamp from .layouts.vertical import VerticalLayout from .message import Message from .messages import CallbackType @@ -1375,10 +1375,20 @@ class Widget(DOMNode): """ try: return self.screen.find_widget(self).region - except NoScreen: - return Region() - except errors.NoWidget: - return Region() + except (NoScreen, errors.NoWidget): + return NULL_REGION + + @property + def dock_gutter(self) -> Spacing: + """Space allocated to docks in the parent. + + Returns: + Space to be subtracted from scrollable area. + """ + try: + return self.screen.find_widget(self).dock_gutter + except (NoScreen, errors.NoWidget): + return NULL_SPACING @property def container_viewport(self) -> Region: @@ -2263,7 +2273,7 @@ class Widget(DOMNode): else: scroll_offset = container.scroll_to_region( region, - spacing=widget.parent.gutter, + spacing=widget.parent.gutter + widget.dock_gutter, animate=animate, speed=speed, duration=duration, diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 018558247..a5c7a6ea6 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -14242,6 +14242,333 @@ ''' # --- +# name: test_dock_scroll2 + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TestApp + + + + + + + + + + TestApp + ───────── + this + is + a + sample + sentence + and + here + are + some + wordsthis + is + a▅▅ + sample + sentence + and + here + are + some + words +  CTRL+Q  Quit  + + + + + + + + ''' +# --- +# name: test_dock_scroll_off_by_one + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollOffByOne + + + + + + + + + + X76 + X77 + X78 + X79 + X80 + X81 + X82 + X83 + X84 + X85 + X86 + X87 + X88 + X89 + X90 + X91 + X92 + X93 + X94▂▂ + X95 + X96 + X97 + X98 + X99 + + + + + + ''' +# --- # name: test_easing_preview ''' @@ -21751,6 +22078,170 @@ ''' # --- +# name: test_scroll_to + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollOffByOne + + + + + + + + + + X27 + X28 + X29 + X30 + X31 + X32 + X33▄▄ + X34 + X35 + X36 + X37 + X38 + X39▂▂ + X40 + X41 + X42 + X43 + X44 + X45 + X46 + X47 + X48 + X49 + X50 + + + + + + ''' +# --- # name: test_scroll_to_center ''' diff --git a/tests/snapshot_tests/snapshot_apps/dock_scroll2.py b/tests/snapshot_tests/snapshot_apps/dock_scroll2.py new file mode 100644 index 000000000..fe2a1b234 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/dock_scroll2.py @@ -0,0 +1,33 @@ +from textual.app import App +from textual.widgets import Header, Label, Footer + + +# Same as dock_scroll.py but with 2 labels +class TestApp(App): + BINDINGS = [("ctrl+q", "app.quit", "Quit")] + CSS = """ + + Label { + border: solid red; + } + Footer { + height: 4; + } + """ + + def compose(self): + text = ( + "this is a sample sentence and here are some words".replace(" ", "\n") * 2 + ) + yield Header() + yield Label(text) + yield Label(text) + yield Footer() + + def on_mount(self): + self.dark = False + + +if __name__ == "__main__": + app = TestApp() + app.run() 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 new file mode 100644 index 000000000..f9a5a00fd --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/dock_scroll_off_by_one.py @@ -0,0 +1,17 @@ +from textual.app import App, ComposeResult +from textual.widgets import Checkbox, Footer + + +class ScrollOffByOne(App): + def compose(self) -> ComposeResult: + for number in range(1, 100): + yield Checkbox(str(number)) + yield Footer() + + def on_mount(self) -> None: + self.query_one("Screen").scroll_end() + + +app = ScrollOffByOne() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css index 50ea9edff..f135a09c5 100644 --- a/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css +++ b/tests/snapshot_tests/snapshot_apps/horizontal_auto_width.css @@ -19,6 +19,6 @@ #horizontal { width: auto; - height: auto; + height: 4; background: darkslateblue; -} \ No newline at end of file +} diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to.py b/tests/snapshot_tests/snapshot_apps/scroll_to.py new file mode 100644 index 000000000..9dd21a3fc --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/scroll_to.py @@ -0,0 +1,19 @@ +from textual.app import App, ComposeResult +from textual.widgets import Checkbox, Footer + + +class ScrollOffByOne(App): + """Scroll to item 50.""" + + def compose(self) -> ComposeResult: + for number in range(1, 100): + yield Checkbox(str(number), id=f"number-{number}") + yield Footer() + + def on_ready(self) -> None: + self.query_one("#number-50").scroll_visible() + + +app = ScrollOffByOne() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 9bb589f7a..0dcd49b19 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -203,9 +203,11 @@ def test_option_list(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_options.py") assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_tables.py") + def test_option_list_build(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "option_list.py") + def test_progress_bar_indeterminate(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["f"]) @@ -457,6 +459,23 @@ def test_dock_scroll(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll.py", terminal_size=(80, 25)) +def test_dock_scroll2(snap_compare): + # https://github.com/Textualize/textual/issues/2525 + assert snap_compare(SNAPSHOT_APPS_DIR / "dock_scroll2.py", terminal_size=(80, 25)) + + +def test_dock_scroll_off_by_one(snap_compare): + # https://github.com/Textualize/textual/issues/2525 + assert snap_compare( + SNAPSHOT_APPS_DIR / "dock_scroll_off_by_one.py", terminal_size=(80, 25) + ) + + +def test_scroll_to(snap_compare): + # https://github.com/Textualize/textual/issues/2525 + assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_to.py", terminal_size=(80, 25)) + + def test_auto_fr(snap_compare): # https://github.com/Textualize/textual/issues/2220 assert snap_compare(SNAPSHOT_APPS_DIR / "auto_fr.py", terminal_size=(80, 25)) From 8753aa5ed0e3c4523ac01630c2a4d5501a15c32f Mon Sep 17 00:00:00 2001 From: Glenn McAllister Date: Tue, 16 May 2023 16:36:24 -0400 Subject: [PATCH 4/5] Update poetry-core requirement (#2572) Related issues: #2562 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index af9942407..56ada690a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,5 +83,5 @@ markers = [ ] [build-system] -requires = ["poetry-core>=1.0.0"] +requires = ["poetry-core>=1.2.0"] build-backend = "poetry.core.masonry.api" From c12fa0e4da15b8005b504d71b73bb5d5cc716566 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 17 May 2023 07:44:36 +0100 Subject: [PATCH 5/5] fix for dark switch (#2585) --- CHANGELOG.md | 1 + src/textual/app.py | 9 +-------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 990180d91..c35703853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535 - `footer--description` component class was being ignored https://github.com/Textualize/textual/issues/2544 - Pasting empty selection in `Input` would raise an exception https://github.com/Textualize/textual/issues/2563 +- Fix for setting dark in App `__init__` https://github.com/Textualize/textual/issues/2583 - Fix issue with scrolling and docks https://github.com/Textualize/textual/issues/2525 ### Added diff --git a/src/textual/app.py b/src/textual/app.py index c8863d764..74585c166 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -604,14 +604,7 @@ class App(Generic[ReturnType], DOMNode): """ self.set_class(dark, "-dark-mode") self.set_class(not dark, "-light-mode") - try: - self.refresh_css() - except ScreenStackError: - # It's possible that `dark` can be set before we have a default - # screen, in an app's `on_load`, for example. So let's eat the - # ScreenStackError -- the above styles will be handled once the - # screen is spun up anyway. - pass + self.call_later(self.refresh_css) def get_driver_class(self) -> Type[Driver]: """Get a driver class for this platform.