| 
 | 1 | +from __future__ import annotations  | 
 | 2 | + | 
 | 3 | +from typing import TYPE_CHECKING  | 
 | 4 | + | 
 | 5 | +from textual.geometry import NULL_OFFSET, Region, Size  | 
 | 6 | +from textual.layout import ArrangeResult, Layout, WidgetPlacement  | 
 | 7 | + | 
 | 8 | +if TYPE_CHECKING:  | 
 | 9 | + | 
 | 10 | +    from textual.widget import Widget  | 
 | 11 | + | 
 | 12 | + | 
 | 13 | +class StreamLayout(Layout):  | 
 | 14 | +    """A cut down version of the vertical layout.  | 
 | 15 | +
  | 
 | 16 | +    The stream layout is faster, but has a few limitations compared to the vertical layout.  | 
 | 17 | +
  | 
 | 18 | +    - All widgets are the full width (as if their widget is `1fr`).  | 
 | 19 | +    - All widgets have an effective height of `auto`.  | 
 | 20 | +    - `max-height` is supported, but only if it is a units value, all other extrema rules are ignored.  | 
 | 21 | +    - No absolute positioning.  | 
 | 22 | +    - No overlay: screen.  | 
 | 23 | +    - Layers are ignored.  | 
 | 24 | +
  | 
 | 25 | +    The primary use of `layout: stream` is for a long list of widgets in a scrolling container, such as  | 
 | 26 | +    what you might expect from a LLM chat-bot. The speed improvement will only be significant with a lot of  | 
 | 27 | +    child widgets, so stick to vertical layouts unless you see any slowdown.  | 
 | 28 | +
  | 
 | 29 | +    """  | 
 | 30 | + | 
 | 31 | +    name = "stream"  | 
 | 32 | + | 
 | 33 | +    def arrange(  | 
 | 34 | +        self, parent: Widget, children: list[Widget], size: Size, greedy: bool = True  | 
 | 35 | +    ) -> ArrangeResult:  | 
 | 36 | +        parent.pre_layout(self)  | 
 | 37 | +        if not children:  | 
 | 38 | +            return []  | 
 | 39 | +        viewport = parent.app.size  | 
 | 40 | + | 
 | 41 | +        _Region = Region  | 
 | 42 | +        _WidgetPlacement = WidgetPlacement  | 
 | 43 | + | 
 | 44 | +        placements: list[WidgetPlacement] = []  | 
 | 45 | +        width = size.width  | 
 | 46 | +        first_child_styles = children[0].styles  | 
 | 47 | +        y = 0  | 
 | 48 | +        previous_margin = first_child_styles.margin.top  | 
 | 49 | +        null_offset = NULL_OFFSET  | 
 | 50 | + | 
 | 51 | +        for widget in children:  | 
 | 52 | +            styles = widget.styles.base  | 
 | 53 | +            margin = styles.margin  | 
 | 54 | +            gutter_width, gutter_height = styles.gutter.totals  | 
 | 55 | +            top, right, bottom, left = margin  | 
 | 56 | +            y += max(top, previous_margin)  | 
 | 57 | +            previous_margin = bottom  | 
 | 58 | +            height = (  | 
 | 59 | +                widget.get_content_height(size, viewport, width - gutter_width)  | 
 | 60 | +                + gutter_height  | 
 | 61 | +            )  | 
 | 62 | +            if (max_height := styles.max_height) is not None and max_height.is_cells:  | 
 | 63 | +                height = min(height, int(max_height.value))  | 
 | 64 | +            placements.append(  | 
 | 65 | +                _WidgetPlacement(  | 
 | 66 | +                    _Region(left, y, width - (left + right), height),  | 
 | 67 | +                    null_offset,  | 
 | 68 | +                    margin,  | 
 | 69 | +                    widget,  | 
 | 70 | +                    0,  | 
 | 71 | +                    False,  | 
 | 72 | +                    False,  | 
 | 73 | +                    False,  | 
 | 74 | +                )  | 
 | 75 | +            )  | 
 | 76 | +            y += height  | 
 | 77 | + | 
 | 78 | +        return placements  | 
0 commit comments