Skip to content

Commit 1f05b1b

Browse files
authored
Merge pull request #6105 from Textualize/layout-optimize
optimize layout
2 parents 48b0f13 + 037790a commit 1f05b1b

File tree

10 files changed

+99
-25
lines changed

10 files changed

+99
-25
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111

1212
- Eager tasks are now enabled On Python3.12 and above https://github.com/Textualize/textual/pull/6102
1313

14-
1514
### Added
1615

1716
- Added `DOMNode.displayed_and_visible_children` https://github.com/Textualize/textual/pull/6102
17+
- Added `Widget.process_layout` https://github.com/Textualize/textual/pull/6105
18+
- Added `App.viewport_size` https://github.com/Textualize/textual/pull/6105
19+
- Added `Screen.size` https://github.com/Textualize/textual/pull/6105
1820

1921
## [6.1.0] - 2025-08-01
2022

src/textual/_arrange.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def _build_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
3131
return layers
3232

3333

34+
_get_dock = attrgetter("styles.is_docked")
35+
_get_split = attrgetter("styles.is_split")
36+
_get_display = attrgetter("display")
37+
38+
3439
def arrange(
3540
widget: Widget,
3641
children: Sequence[Widget],
@@ -50,22 +55,17 @@ def arrange(
5055
"""
5156

5257
placements: list[WidgetPlacement] = []
53-
scroll_spacing = Spacing()
54-
55-
get_dock = attrgetter("styles.is_docked")
56-
get_split = attrgetter("styles.is_split")
57-
get_display = attrgetter("styles.display")
58-
58+
scroll_spacing = NULL_SPACING
5959
styles = widget.styles
6060

6161
# Widgets which will be displayed
62-
display_widgets = [child for child in children if get_display(child) != "none"]
62+
display_widgets = list(filter(_get_display, children))
6363
# Widgets organized into layers
6464
layers = _build_layers(display_widgets)
6565

6666
for widgets in layers.values():
6767
# Partition widgets into split widgets and non-split widgets
68-
non_split_widgets, split_widgets = partition(get_split, widgets)
68+
non_split_widgets, split_widgets = partition(_get_split, widgets)
6969
if split_widgets:
7070
_split_placements, dock_region = _arrange_split_widgets(
7171
split_widgets, size, viewport
@@ -78,7 +78,7 @@ def arrange(
7878

7979
# Partition widgets into "layout" widgets (those that appears in the normal 'flow' of the
8080
# document), and "dock" widgets which are positioned relative to an edge
81-
layout_widgets, dock_widgets = partition(get_dock, non_split_widgets)
81+
layout_widgets, dock_widgets = partition(_get_dock, non_split_widgets)
8282

8383
# Arrange docked widgets
8484
if dock_widgets:
@@ -94,8 +94,10 @@ def arrange(
9494

9595
if layout_widgets:
9696
# Arrange layout widgets (i.e. not docked)
97-
layout_placements = widget.layout.arrange(
98-
widget, layout_widgets, dock_region.size, greedy=not optimal
97+
layout_placements = widget.process_layout(
98+
widget.layout.arrange(
99+
widget, layout_widgets, dock_region.size, greedy=not optimal
100+
)
99101
)
100102
scroll_spacing = scroll_spacing.grow_maximum(dock_spacing)
101103
placement_offset = dock_region.offset

src/textual/_profile.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,17 @@
1010

1111

1212
@contextlib.contextmanager
13-
def timer(subject: str = "time") -> Generator[None, None, None]:
14-
"""print the elapsed time. (only used in debugging)"""
13+
def timer(subject: str = "time", threshold: float = 0) -> Generator[None, None, None]:
14+
"""print the elapsed time. (only used in debugging).
15+
16+
Args:
17+
subject: Text shown in log.
18+
threshold: Time in second after which the log is written.
19+
20+
"""
1521
start = perf_counter()
1622
yield
1723
elapsed = perf_counter() - start
18-
elapsed_ms = elapsed * 1000
19-
log(f"{subject} elapsed {elapsed_ms:.4f}ms")
24+
if elapsed >= threshold:
25+
elapsed_ms = elapsed * 1000
26+
log(f"{subject} elapsed {elapsed_ms:.4f}ms")

src/textual/app.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,14 @@ def size(self) -> Size:
15651565
width, height = self.console.size
15661566
return Size(width, height)
15671567

1568+
@property
1569+
def viewport_size(self) -> Size:
1570+
"""Get the viewport size (size of the screen)."""
1571+
try:
1572+
return self.screen.size
1573+
except (ScreenStackError, NoScreen):
1574+
return self.size
1575+
15681576
def _get_inline_height(self) -> int:
15691577
"""Get the inline height (height when in inline mode).
15701578

src/textual/layouts/grid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def arrange(
6060
table_size_columns -= 1
6161

6262
table_size_rows = styles.grid_size_rows
63-
viewport = parent.screen.size
63+
viewport = parent.app.viewport_size
6464
keyline_style, _keyline_color = styles.keyline
6565
offset = (0, 0)
6666
gutter_spacing: Spacing | None

src/textual/layouts/horizontal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def arrange(
2525
parent.pre_layout(self)
2626
placements: list[WidgetPlacement] = []
2727
add_placement = placements.append
28-
viewport = parent.app.size
28+
viewport = parent.app.viewport_size
2929

3030
child_styles = [child.styles for child in children]
3131
box_margins: list[Spacing] = [

src/textual/layouts/stream.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def arrange(
3737
parent.pre_layout(self)
3838
if not children:
3939
return []
40-
viewport = parent.app.size
40+
viewport = parent.app.viewport_size
4141

4242
_Region = Region
4343
_WidgetPlacement = WidgetPlacement
@@ -81,3 +81,37 @@ def arrange(
8181
y += height
8282

8383
return placements
84+
85+
def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int:
86+
"""Get the optimal content width by arranging children.
87+
88+
Args:
89+
widget: The container widget.
90+
container: The container size.
91+
viewport: The viewport size.
92+
93+
Returns:
94+
Width of the content.
95+
"""
96+
return widget.scrollable_content_region.width
97+
98+
def get_content_height(
99+
self, widget: Widget, container: Size, viewport: Size, width: int
100+
) -> int:
101+
"""Get the content height.
102+
103+
Args:
104+
widget: The container widget.
105+
container: The container size.
106+
viewport: The viewport.
107+
width: The content width.
108+
109+
Returns:
110+
Content height (in lines).
111+
"""
112+
if widget._nodes:
113+
arrangement = widget._arrange(Size(width, 0))
114+
height = arrangement.total_region.height
115+
else:
116+
height = 0
117+
return height

src/textual/layouts/vertical.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def arrange(
2323
parent.pre_layout(self)
2424
placements: list[WidgetPlacement] = []
2525
add_placement = placements.append
26-
viewport = parent.app.size
26+
viewport = parent.app.viewport_size
2727

2828
child_styles = [child.styles for child in children]
2929
box_margins: list[Spacing] = [

src/textual/screen.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,11 @@ def layers(self) -> tuple[str, ...]:
341341
extras.append("_tooltips")
342342
return (*super().layers, *extras)
343343

344+
@property
345+
def size(self) -> Size:
346+
"""The size of the screen."""
347+
return self.app.size - self.styles.gutter.totals
348+
344349
def _watch_focused(self):
345350
self.refresh_bindings()
346351

@@ -512,7 +517,7 @@ def get_maximize_widgets(maximized: Widget) -> list[Widget]:
512517
else self._nodes
513518
),
514519
size,
515-
self.screen.size,
520+
self.size,
516521
False,
517522
)
518523

src/textual/widget.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
Spacing,
8080
clamp,
8181
)
82-
from textual.layout import Layout
82+
from textual.layout import Layout, WidgetPlacement
8383
from textual.layouts.vertical import VerticalLayout
8484
from textual.message import Message
8585
from textual.messages import CallbackType, Prune
@@ -460,7 +460,7 @@ def __init__(
460460
self._content_height_cache: tuple[object, int] = (None, 0)
461461

462462
self._arrangement_cache: FIFOCache[
463-
tuple[Size, int, Widget], DockArrangeResult
463+
tuple[Size, int, Widget | None], DockArrangeResult
464464
] = FIFOCache(4)
465465

466466
self._styles_cache = StylesCache()
@@ -605,7 +605,7 @@ def offset(self) -> Offset:
605605
Returns:
606606
Relative offset.
607607
"""
608-
return self.styles.offset.resolve(self.size, self.app.size)
608+
return self.styles.offset.resolve(self.size, self.screen.size)
609609

610610
@offset.setter
611611
def offset(self, offset: tuple[int, int]) -> None:
@@ -717,10 +717,26 @@ def _cover(self, widget: Widget) -> None:
717717
self._uncover()
718718
self._cover_widget = widget
719719
widget._parent = self
720+
widget._start_messages()
720721
widget._post_register(self.app)
721722
self.app.stylesheet.apply(widget)
722723
self.refresh(layout=True)
723-
widget._start_messages()
724+
725+
def process_layout(
726+
self, placements: list[WidgetPlacement]
727+
) -> list[WidgetPlacement]:
728+
"""A hook to allow for the manipulation of widget placements before rendering.
729+
730+
You could use this as a way to modify the positions / margins of widgets if your requirement is
731+
not supported in TCSS. In practice, this method is rarely needed!
732+
733+
Args:
734+
placements: A list of [`WidgetPlacement`][textual.layout.WidgetPlacement] objects.
735+
736+
Returns:
737+
A new list of placements.
738+
"""
739+
return placements
724740

725741
def _uncover(self) -> None:
726742
"""Remove any widget, previously set via [`_cover`][textual.widget.Widget._cover]."""

0 commit comments

Comments
 (0)