Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

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


### Added

- Added `DOMNode.displayed_and_visible_children` https://github.com/Textualize/textual/pull/6102
- Added `Widget.process_layout` https://github.com/Textualize/textual/pull/6105
- Added `App.viewport_size` https://github.com/Textualize/textual/pull/6105
- Added `Screen.size` https://github.com/Textualize/textual/pull/6105

## [6.1.0] - 2025-08-01

Expand Down
24 changes: 13 additions & 11 deletions src/textual/_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def _build_layers(widgets: Iterable[Widget]) -> Mapping[str, Sequence[Widget]]:
return layers


_get_dock = attrgetter("styles.is_docked")
_get_split = attrgetter("styles.is_split")
_get_display = attrgetter("display")


def arrange(
widget: Widget,
children: Sequence[Widget],
Expand All @@ -50,22 +55,17 @@ def arrange(
"""

placements: list[WidgetPlacement] = []
scroll_spacing = Spacing()

get_dock = attrgetter("styles.is_docked")
get_split = attrgetter("styles.is_split")
get_display = attrgetter("styles.display")

scroll_spacing = NULL_SPACING
styles = widget.styles

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

for widgets in layers.values():
# Partition widgets into split widgets and non-split widgets
non_split_widgets, split_widgets = partition(get_split, widgets)
non_split_widgets, split_widgets = partition(_get_split, widgets)
if split_widgets:
_split_placements, dock_region = _arrange_split_widgets(
split_widgets, size, viewport
Expand All @@ -78,7 +78,7 @@ def arrange(

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

# Arrange docked widgets
if dock_widgets:
Expand All @@ -94,8 +94,10 @@ def arrange(

if layout_widgets:
# Arrange layout widgets (i.e. not docked)
layout_placements = widget.layout.arrange(
widget, layout_widgets, dock_region.size, greedy=not optimal
layout_placements = widget.process_layout(
widget.layout.arrange(
widget, layout_widgets, dock_region.size, greedy=not optimal
)
)
scroll_spacing = scroll_spacing.grow_maximum(dock_spacing)
placement_offset = dock_region.offset
Expand Down
15 changes: 11 additions & 4 deletions src/textual/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@


@contextlib.contextmanager
def timer(subject: str = "time") -> Generator[None, None, None]:
"""print the elapsed time. (only used in debugging)"""
def timer(subject: str = "time", threshold: float = 0) -> Generator[None, None, None]:
"""print the elapsed time. (only used in debugging).

Args:
subject: Text shown in log.
threshold: Time in second after which the log is written.

"""
start = perf_counter()
yield
elapsed = perf_counter() - start
elapsed_ms = elapsed * 1000
log(f"{subject} elapsed {elapsed_ms:.4f}ms")
if elapsed >= threshold:
elapsed_ms = elapsed * 1000
log(f"{subject} elapsed {elapsed_ms:.4f}ms")
8 changes: 8 additions & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1565,6 +1565,14 @@ def size(self) -> Size:
width, height = self.console.size
return Size(width, height)

@property
def viewport_size(self) -> Size:
"""Get the viewport size (size of the screen)."""
try:
return self.screen.size
except (ScreenStackError, NoScreen):
return self.size

def _get_inline_height(self) -> int:
"""Get the inline height (height when in inline mode).

Expand Down
2 changes: 1 addition & 1 deletion src/textual/layouts/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def arrange(
table_size_columns -= 1

table_size_rows = styles.grid_size_rows
viewport = parent.screen.size
viewport = parent.app.viewport_size
keyline_style, _keyline_color = styles.keyline
offset = (0, 0)
gutter_spacing: Spacing | None
Expand Down
2 changes: 1 addition & 1 deletion src/textual/layouts/horizontal.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def arrange(
parent.pre_layout(self)
placements: list[WidgetPlacement] = []
add_placement = placements.append
viewport = parent.app.size
viewport = parent.app.viewport_size

child_styles = [child.styles for child in children]
box_margins: list[Spacing] = [
Expand Down
36 changes: 35 additions & 1 deletion src/textual/layouts/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def arrange(
parent.pre_layout(self)
if not children:
return []
viewport = parent.app.size
viewport = parent.app.viewport_size

_Region = Region
_WidgetPlacement = WidgetPlacement
Expand Down Expand Up @@ -81,3 +81,37 @@ def arrange(
y += height

return placements

def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int:
"""Get the optimal content width by arranging children.

Args:
widget: The container widget.
container: The container size.
viewport: The viewport size.

Returns:
Width of the content.
"""
return widget.scrollable_content_region.width

def get_content_height(
self, widget: Widget, container: Size, viewport: Size, width: int
) -> int:
"""Get the content height.

Args:
widget: The container widget.
container: The container size.
viewport: The viewport.
width: The content width.

Returns:
Content height (in lines).
"""
if widget._nodes:
arrangement = widget._arrange(Size(width, 0))
height = arrangement.total_region.height
else:
height = 0
return height
2 changes: 1 addition & 1 deletion src/textual/layouts/vertical.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def arrange(
parent.pre_layout(self)
placements: list[WidgetPlacement] = []
add_placement = placements.append
viewport = parent.app.size
viewport = parent.app.viewport_size

child_styles = [child.styles for child in children]
box_margins: list[Spacing] = [
Expand Down
7 changes: 6 additions & 1 deletion src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ def layers(self) -> tuple[str, ...]:
extras.append("_tooltips")
return (*super().layers, *extras)

@property
def size(self) -> Size:
"""The size of the screen."""
return self.app.size - self.styles.gutter.totals

def _watch_focused(self):
self.refresh_bindings()

Expand Down Expand Up @@ -512,7 +517,7 @@ def get_maximize_widgets(maximized: Widget) -> list[Widget]:
else self._nodes
),
size,
self.screen.size,
self.size,
False,
)

Expand Down
24 changes: 20 additions & 4 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
Spacing,
clamp,
)
from textual.layout import Layout
from textual.layout import Layout, WidgetPlacement
from textual.layouts.vertical import VerticalLayout
from textual.message import Message
from textual.messages import CallbackType, Prune
Expand Down Expand Up @@ -460,7 +460,7 @@ def __init__(
self._content_height_cache: tuple[object, int] = (None, 0)

self._arrangement_cache: FIFOCache[
tuple[Size, int, Widget], DockArrangeResult
tuple[Size, int, Widget | None], DockArrangeResult
] = FIFOCache(4)

self._styles_cache = StylesCache()
Expand Down Expand Up @@ -605,7 +605,7 @@ def offset(self) -> Offset:
Returns:
Relative offset.
"""
return self.styles.offset.resolve(self.size, self.app.size)
return self.styles.offset.resolve(self.size, self.screen.size)

@offset.setter
def offset(self, offset: tuple[int, int]) -> None:
Expand Down Expand Up @@ -717,10 +717,26 @@ def _cover(self, widget: Widget) -> None:
self._uncover()
self._cover_widget = widget
widget._parent = self
widget._start_messages()
widget._post_register(self.app)
self.app.stylesheet.apply(widget)
self.refresh(layout=True)
widget._start_messages()

def process_layout(
self, placements: list[WidgetPlacement]
) -> list[WidgetPlacement]:
"""A hook to allow for the manipulation of widget placements before rendering.

You could use this as a way to modify the positions / margins of widgets if your requirement is
not supported in TCSS. In practice, this method is rarely needed!

Args:
placements: A list of [`WidgetPlacement`][textual.layout.WidgetPlacement] objects.

Returns:
A new list of placements.
"""
return placements

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