diff --git a/src/textual/_layout.py b/src/textual/_layout.py index c263c13ee..e7bd0bf4b 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -46,7 +46,8 @@ class Layout(ABC): """ def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int: - """Get the width of the content. + """Get the width of the content. In Horizontal layout, the content width of + a widget is the sum of the widths of its children. Args: widget (Widget): The container widget. @@ -56,19 +57,17 @@ class Layout(ABC): Returns: int: Width of the content. """ - width: int | None = None - gutter_width = widget.gutter.width - for child in widget.displayed_children: - if not child.is_container: - child_width = ( - child.get_content_width(container, viewport) - + gutter_width - + child.gutter.width - ) - width = child_width if width is None else max(width, child_width) - if width is None: + if not widget.children: width = 0 - + else: + placements, _, _ = widget._arrange(Size(0, 0)) + width = max( + [ + placement.region.right + placement.margin.right + for placement in placements + ], + default=0, + ) return width def get_content_height( @@ -85,12 +84,16 @@ class Layout(ABC): Returns: int: Content height (in lines). """ - if not widget.displayed_children: + if not widget.children: height = 0 else: - placements, *_ = widget._arrange(Size(width, container.height)) + placements, _, _ = widget._arrange(Size(width, 0)) height = max( - placement.region.bottom + placement.margin.bottom - for placement in placements + [ + placement.region.bottom + placement.margin.bottom + for placement in placements + ], + default=0, ) + return height diff --git a/src/textual/cli/previews/colors.css b/src/textual/cli/previews/colors.css index 657a8aabb..caa950430 100644 --- a/src/textual/cli/previews/colors.css +++ b/src/textual/cli/previews/colors.css @@ -1,3 +1,6 @@ +Label { + width: 100%; +} ColorButtons { dock: left; diff --git a/src/textual/demo.css b/src/textual/demo.css index c6b7fecad..620cae5f0 100644 --- a/src/textual/demo.css +++ b/src/textual/demo.css @@ -190,7 +190,7 @@ LocationLink { padding: 1 2; background: $boost; color: $text; - + box-sizing: content-box; content-align: center middle; } diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index 08d5f7077..ac643663e 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -59,25 +59,3 @@ class HorizontalLayout(Layout): x = next_x + margin return placements, set(displayed_children) - - def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int: - """Get the width of the content. In Horizontal layout, the content width of - a widget is the sum of the widths of its children. - - Args: - widget (Widget): The container widget. - container (Size): The container size. - viewport (Size): The viewport size. - - Returns: - int: Width of the content. - """ - if not widget.displayed_children: - width = 0 - else: - placements, *_ = widget._arrange(container) - width = max( - placement.region.right + placement.margin.right - for placement in placements - ) - return width diff --git a/src/textual/widget.py b/src/textual/widget.py index 4011925f7..4a68ce032 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -37,6 +37,7 @@ from rich.text import Text from . import errors, events, messages from ._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction from ._arrange import DockArrangeResult, arrange +from ._cache import LRUCache from ._context import active_app from ._easing import DEFAULT_SCROLL_EASING from ._layout import Layout @@ -246,8 +247,8 @@ class Widget(DOMNode): self._content_width_cache: tuple[object, int] = (None, 0) self._content_height_cache: tuple[object, int] = (None, 0) - self._arrangement: DockArrangeResult | None = None - self._arrangement_cache_key: tuple[int, Size] = (-1, Size()) + self._arrangement_cache_updates: int = -1 + self._arrangement_cache: LRUCache[Size, DockArrangeResult] = LRUCache(4) self._styles_cache = StylesCache() self._rich_style_cache: dict[str, tuple[Style, Style]] = {} @@ -457,21 +458,25 @@ class Widget(DOMNode): Returns: ArrangeResult: Widget locations. """ + assert self.is_container - arrange_cache_key = (self.children._updates, size) - if ( - self._arrangement is not None - and arrange_cache_key == self._arrangement_cache_key - ): - return self._arrangement + if self._arrangement_cache_updates != self.children._updates: + self._arrangement_cache_updates = self.children._updates + self._arrangement_cache.clear() - self._arrangement_cache_key = arrange_cache_key - self._arrangement = arrange(self, self.children, size, self.screen.size) - return self._arrangement + cached_arrangement = self._arrangement_cache.get(size, None) + if cached_arrangement is not None: + return cached_arrangement + + arrangement = self._arrangement_cache[size] = arrange( + self, self.children, size, self.screen.size + ) + return arrangement def _clear_arrangement_cache(self) -> None: """Clear arrangement cache, forcing a new arrange operation.""" - self._arrangement = None + # self._arrangement = None + self._arrangement_cache.clear() def _get_virtual_dom(self) -> Iterable[Widget]: """Get widgets not part of the DOM. diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index d950c09a7..a7edd1f43 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -344,6 +344,163 @@ ''' # --- +# name: test_columns_height + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HeightApp + + + + + + + + + + ┌──────────────────────────────────────────────────────────────────────────────┐ + ┌────────────────────┐┌────────────────┐┌──────────────────────┐ + As tall as containerThis has defaultI have a static height + height + but a + few lines + └────────────────┘ + + + + + + + + + + └────────────────────┘└──────────────────────┘ + └──────────────────────────────────────────────────────────────────────────────┘ + + + + + + + + + + + ''' +# --- # name: test_css_property[align.py] ''' @@ -6637,6 +6794,324 @@ ''' # --- +# name: test_order_independence + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Layers + + + + + + + + + + ┌──────────────────────────────────┐Layers + It's full of stars! My God! It's full of sta + + This should float over the top + + + └──────────────────────────────────┘ + + + + + + + + + + + + + + + + +  T  Toggle Screen  + + + + + ''' +# --- +# name: test_order_independence_toggle + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Layers + + + + + + + + + + ┌──────────────────────────────────┐ + It's full of stars! My God! It's full of sta + + This should float over the top + + + └──────────────────────────────────┘ + + + + + + + + + + + + + + + + +  T  Toggle Screen  + + + + + ''' +# --- # name: test_placeholder_render ''' @@ -6807,324 +7282,6 @@ ''' # --- -# name: test_order_independence - ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Layers - - - - - - - - - - ┌────────────────────────────────────────┐ - ull of stars! My God! It's full of sta - - This should float over the top - - - └────────────────────────────────────────┘ - - - - - - - - - - - - - - - - -  T  Toggle Screen  - - - - - ''' -# --- -# name: test_order_independence_toggle - ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Layers - - - - - - - - - - ┌────────────────────────────────────────┐ - t. I'm sorry, Dave. I'm afraid I can't - - This should float over the top - - - └────────────────────────────────────────┘ - - - - - - - - - - - - - - - - -  T  Toggle Screen  - - - - - ''' -# --- # name: test_textlog_max_lines ''' diff --git a/tests/snapshot_tests/snapshot_apps/columns_height.py b/tests/snapshot_tests/snapshot_apps/columns_height.py new file mode 100644 index 000000000..6565f1da4 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/columns_height.py @@ -0,0 +1,37 @@ +from textual.app import App, ComposeResult +from textual.containers import Horizontal +from textual.widgets import Static + + +class HeightApp(App[None]): + + CSS = """ + Horizontal { + border: solid red; + height: auto; + } + + Static { + border: solid green; + width: auto; + } + + #fill_parent { + height: 100%; + } + + #static { + height: 16; + } + """ + + def compose(self) -> ComposeResult: + yield Horizontal( + Static("As tall as container", id="fill_parent"), + Static("This has default\nheight\nbut a\nfew lines"), + Static("I have a static height", id="static"), + ) + + +if __name__ == "__main__": + HeightApp().run() diff --git a/tests/snapshot_tests/snapshot_apps/order_independence.py b/tests/snapshot_tests/snapshot_apps/order_independence.py index 0c682c859..2ad43e931 100644 --- a/tests/snapshot_tests/snapshot_apps/order_independence.py +++ b/tests/snapshot_tests/snapshot_apps/order_independence.py @@ -16,7 +16,7 @@ class Body1(Vertical): class Body2(Vertical): def compose(self) -> ComposeResult: - yield Label("I'm sorry, Dave. I'm afraid I can't do that. " * 300) + yield Label("My God! It's full of stars! " * 300) class Good(Screen): @@ -52,6 +52,10 @@ class Layers(App[None]): background: red; color: yellow; } + + Body2 { + background: green; + } """ SCREENS = {"good": Good, "bad": Bad} diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index e63a697c4..21821b330 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -139,15 +139,18 @@ def test_multiple_css(snap_compare): def test_order_independence(snap_compare): - # Interaction between multiple CSS files and app-level/classvar CSS assert snap_compare("snapshot_apps/order_independence.py") def test_order_independence_toggle(snap_compare): - # Interaction between multiple CSS files and app-level/classvar CSS assert snap_compare("snapshot_apps/order_independence.py", press="t,_") +def test_columns_height(snap_compare): + # Interaction with height auto, and relative heights to make columns + assert snap_compare("snapshot_apps/columns_height.py") + + # --- Other ---