From 925c520318e4f60760a752d12364faacf9c6668c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 1 Aug 2025 09:29:21 +0100 Subject: [PATCH] Stream layout --- .gitignore | 1 + src/textual/css/constants.py | 2 +- src/textual/css/styles.py | 2 +- src/textual/layouts/factory.py | 2 ++ src/textual/layouts/stream.py | 58 ++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/textual/layouts/stream.py diff --git a/.gitignore b/.gitignore index b3307c931..d3b2bcb96 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ lib/ lib64/ parts/ sdist/ +dist/ var/ wheels/ *.egg-info/ diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 01cd168f9..7928dfd90 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -28,7 +28,7 @@ VALID_BORDER: Final = { "wide", } VALID_EDGE: Final = {"top", "right", "bottom", "left", "none"} -VALID_LAYOUT: Final = {"vertical", "horizontal", "grid"} +VALID_LAYOUT: Final = {"vertical", "horizontal", "grid", "stream"} VALID_BOX_SIZING: Final = {"border-box", "content-box"} VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"} diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index d306d4276..b613e21c8 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -284,7 +284,7 @@ class StylesBase: layout = LayoutProperty() """Set the layout of the widget, defining how it's children are laid out. - Valid values are "grid", "horizontal", and "vertical" or None to clear any layout + Valid values are "grid", "stream", "horizontal", or "vertical" or None to clear any layout that was set at runtime. Raises: diff --git a/src/textual/layouts/factory.py b/src/textual/layouts/factory.py index 866736373..dc09b9575 100644 --- a/src/textual/layouts/factory.py +++ b/src/textual/layouts/factory.py @@ -3,12 +3,14 @@ from __future__ import annotations from textual.layout import Layout from textual.layouts.grid import GridLayout from textual.layouts.horizontal import HorizontalLayout +from textual.layouts.stream import StreamLayout from textual.layouts.vertical import VerticalLayout LAYOUT_MAP: dict[str, type[Layout]] = { "horizontal": HorizontalLayout, "grid": GridLayout, "vertical": VerticalLayout, + "stream": StreamLayout, } diff --git a/src/textual/layouts/stream.py b/src/textual/layouts/stream.py new file mode 100644 index 000000000..e1ff403bb --- /dev/null +++ b/src/textual/layouts/stream.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from textual.geometry import NULL_OFFSET, Region, Size +from textual.layout import ArrangeResult, Layout, WidgetPlacement + +if TYPE_CHECKING: + + from textual.widget import Widget + + +class StreamLayout(Layout): + name = "stream" + + def arrange( + self, parent: Widget, children: list[Widget], size: Size, greedy: bool = True + ) -> ArrangeResult: + parent.pre_layout(self) + viewport = parent.app.size + + placements: list[WidgetPlacement] = [] + if not children: + return [] + width = size.width + first_child_styles = children[0].styles + y = first_child_styles.margin.top + previous_margin = 0 + null_offset = NULL_OFFSET + + for widget in children: + styles = widget.styles + overlay = styles.overlay == "screen" + absolute = styles.has_rule("position") and styles.position == "absolute" + margin = styles.margin + top, right, bottom, left = margin + margin_width = left + right + y += max(top, previous_margin) + previous_margin = bottom + height = widget.get_content_height(size, viewport, width - margin_width) + height += styles.gutter.height + if (max_height := styles.max_height) is not None and max_height.is_cells: + height = min(height, int(max_height.value)) + placements.append( + WidgetPlacement( + Region(left, y, width - margin_width, height), + null_offset, + margin, + widget, + 0, + False, + overlay, + absolute, + ) + ) + y += height + + return placements