From 875d67a91d6e7cc8bdaadce9117a0a302ee7128f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 19 May 2022 17:48:39 +0100 Subject: [PATCH] fixes for layout --- sandbox/basic.css | 6 ++--- sandbox/nest.css | 25 +++++++++++++++++++ sandbox/nest.py | 38 +++++++++++++++++++++++++++++ src/textual/_border.py | 2 +- src/textual/_layout.py | 1 - src/textual/box_model.py | 32 ++++++++++++------------ src/textual/layouts/dock.py | 7 +++++- src/textual/layouts/vertical.py | 4 +-- src/textual/widget.py | 43 +++++++++++++++++---------------- 9 files changed, 111 insertions(+), 47 deletions(-) create mode 100644 sandbox/nest.css create mode 100644 sandbox/nest.py diff --git a/sandbox/basic.css b/sandbox/basic.css index f70b42a82..16e2220e4 100644 --- a/sandbox/basic.css +++ b/sandbox/basic.css @@ -69,7 +69,7 @@ App > Screen { color: $text-background; background: $background; layout: vertical; - overflow-y: scroll; + overflow-y: scroll; } @@ -114,11 +114,11 @@ TweetHeader { } TweetBody { - width: 100%; + width: 100w; background: $panel; color: $text-panel; height: auto; - padding: 0 1 0 0; + padding: 0 1 0 0; } diff --git a/sandbox/nest.css b/sandbox/nest.css new file mode 100644 index 000000000..63abf3504 --- /dev/null +++ b/sandbox/nest.css @@ -0,0 +1,25 @@ + + +Vertical { + background: blue; + +} + +#container { + width:50%; + height: auto; + align-horizontal: center; + padding: 1; + border: heavy white; + background: white 50%; + overflow-y: auto +} + +TextWidget { + /* width: 50%; */ + height: auto; + padding: 2; + background: green 30%; + border: yellow; + box-sizing: border-box; +} \ No newline at end of file diff --git a/sandbox/nest.py b/sandbox/nest.py new file mode 100644 index 000000000..263d7c2da --- /dev/null +++ b/sandbox/nest.py @@ -0,0 +1,38 @@ +from textual.app import App, ComposeResult +from textual.widget import Widget +from textual import layout + + +from rich.text import Text + + +lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + +TEXT = Text.from_markup(lorem) + + +class TextWidget(Widget): + def render(self, style): + return TEXT + + +class AutoApp(App): + def on_mount(self) -> None: + self.bind("t", "tree") + + def compose(self) -> ComposeResult: + yield layout.Vertical( + Widget( + TextWidget(classes="test"), + id="container", + ), + ) + + def action_tree(self): + self.log(self.screen.tree) + + +app = AutoApp(css_path="nest.css") + +if __name__ == "__main__": + app.run() diff --git a/src/textual/_border.py b/src/textual/_border.py index 139aca33f..6fb93d92a 100644 --- a/src/textual/_border.py +++ b/src/textual/_border.py @@ -166,7 +166,7 @@ class Border: if has_top: lines.pop(0) - if has_bottom: + if has_bottom and lines: lines.pop(-1) divide = Segment.divide diff --git a/src/textual/_layout.py b/src/textual/_layout.py index 3f629d1e6..53269be0a 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -60,7 +60,6 @@ class Layout(ABC): """ width: int | None = None for child in widget.displayed_children: - assert isinstance(child, Widget) if not child.is_container: child_width = child.get_content_width(container, viewport) width = child_width if width is None else max(width, child_width) diff --git a/src/textual/box_model.py b/src/textual/box_model.py index 3bbf1befa..d34ddd2ef 100644 --- a/src/textual/box_model.py +++ b/src/textual/box_model.py @@ -35,50 +35,48 @@ def get_box_model( width, height = container is_content_box = styles.box_sizing == "content-box" is_border_box = styles.box_sizing == "border-box" - gutter = styles.padding + styles.border.spacing + gutter = styles.gutter margin = styles.margin is_auto_width = styles.width and styles.width.is_auto + is_auto_height = styles.height and styles.height.is_auto + + # Container minus padding and border + content_container = container - gutter.totals + # The container including the content + sizing_container = content_container if is_border_box else container if styles.width is None: width = container.width - margin.width elif is_auto_width: # When width is auto, we want enough space to always fit the content - width = get_content_width( - (container - gutter.totals if is_border_box else container) - - styles.margin.totals, - viewport, - ) + width = get_content_width(sizing_container - styles.margin.totals, viewport) else: width = styles.width.resolve_dimension(container, viewport) if styles.min_width is not None: - min_width = styles.min_width.resolve_dimension(container, viewport) + min_width = styles.min_width.resolve_dimension(content_container, viewport) width = max(width, min_width) if styles.max_width is not None: - max_width = styles.max_width.resolve_dimension(container, viewport) + max_width = styles.max_width.resolve_dimension(content_container, viewport) width = min(width, max_width) if styles.height is None: height = container.height - margin.height elif styles.height.is_auto: - height = get_content_height( - container - gutter.totals if is_border_box else container, - viewport, - (width - gutter.width) if is_border_box else width, - ) + height = get_content_height(content_container, viewport, width - gutter.width) if is_border_box: height += gutter.height else: - height = styles.height.resolve_dimension(container, viewport) + height = styles.height.resolve_dimension(content_container, viewport) if styles.min_height is not None: - min_height = styles.min_height.resolve_dimension(container, viewport) + min_height = styles.min_height.resolve_dimension(content_container, viewport) height = max(height, min_height) if styles.max_height is not None: - max_height = styles.max_height.resolve_dimension(container, viewport) + max_height = styles.max_height.resolve_dimension(content_container, viewport) height = min(height, max_height) if is_border_box and is_auto_width: @@ -86,7 +84,7 @@ def get_box_model( if is_content_box: width += gutter.width - height += gutter.height + # height += gutter.height model = BoxModel(Size(width, height), margin) return model diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index 04bd62c9d..e1ce2b7c2 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -26,9 +26,13 @@ DockEdge = Literal["top", "right", "bottom", "left"] @dataclass class DockOptions: size: int | None = None - fraction: int = 1 + fraction: int | None = 1 min_size: int = 1 + def __post_init__(self) -> None: + if self.size is None and self.fraction is None: + self.fraction = 1 + class Dock(NamedTuple): edge: Edge @@ -71,6 +75,7 @@ class DockLayout(Layout): styles = widget.styles has_rule = styles.has_rule + # TODO: This was written pre resolve_dimension, we should update this to use available units return ( DockOptions( styles.width.cells if has_rule("width") else None, diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index ae7bc05b3..e216f963d 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -19,7 +19,6 @@ class VerticalLayout(Layout): placements: list[WidgetPlacement] = [] add_placement = placements.append - max_width = max_height = 0 parent_size = parent.size box_models = [ @@ -43,9 +42,8 @@ class VerticalLayout(Layout): region = Region(offset_x, y, content_width, content_height) add_placement(WidgetPlacement(region, widget, 0)) y += region.height + margin - max_height = y - total_region = Region(0, 0, max_width, max_height) + total_region = Region(0, 0, size.width, y) add_placement(WidgetPlacement(total_region, None, 0)) return placements, set(displayed_children) diff --git a/src/textual/widget.py b/src/textual/widget.py index 3ca3dcbcd..12d91b0bf 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -66,13 +66,12 @@ class RenderCache(NamedTuple): @rich.repr.auto class Widget(DOMNode): + CSS = """ + """ + can_focus: bool = False can_focus_children: bool = True - CSS = """ - - """ - def __init__( self, *children: Widget, @@ -169,14 +168,18 @@ class Widget(DOMNode): self.get_content_width, self.get_content_height, ) + print("--") + print(self) + print("container", container) + print("model", box_model) return box_model def get_content_width(self, container: Size, viewport: Size) -> int: """Gets the width of the content area. Args: - container_size (Size): Size of the container (immediate parent) widget. - viewport_size (Size): Size of the viewport. + container (Size): Size of the container (immediate parent) widget. + viewport (Size): Size of the viewport. Returns: int: The optimal width of the content. @@ -194,32 +197,34 @@ class Widget(DOMNode): width = measurement.maximum return width - def get_content_height( - self, container_size: Size, viewport_size: Size, width: int - ) -> int: + def get_content_height(self, container: Size, viewport: Size, width: int) -> int: """Gets the height (number of lines) in the content area. Args: - container_size (Size): Size of the container (immediate parent) widget. - viewport_size (Size): Size of the viewport. + container (Size): Size of the container (immediate parent) widget. + viewport (Size): Size of the viewport. width (int): Width of renderable. Returns: int: The height of the content. """ - if self.is_container: assert self.layout is not None height = self.layout.get_content_height( - self, container_size, viewport_size, width + self, + container, + viewport, + width, ) + print(self, "auto_height container", "width=", width, "height=", height) else: renderable = self.render(self.styles.rich_style) options = self.console.options.update_width(width).update(highlight=False) - segments = self.console.render(renderable, options) # Cheaper than counting the lines returned from render_lines! height = sum(text.count("\n") for text, _, _ in segments) + print(self, "auto_height renderable", height) + return height async def watch_scroll_x(self, new_value: float) -> None: @@ -690,13 +695,9 @@ class Widget(DOMNode): return self._animate @property - def layout(self) -> Layout | None: - return self.styles.layout or ( - # If we have children we _should_ return a layout, otherwise they won't be displayed: - self._default_layout - if self.children - else None - ) + def layout(self) -> Layout: + """Get the layout object if set in styles, or a default layout.""" + return self.styles.layout or self._default_layout @property def is_container(self) -> bool: