From 62f7ed83582d791f06edf3b8566b4bb0643ef542 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 16 Aug 2022 17:06:33 +0100 Subject: [PATCH] alignment fix --- sandbox/will/screens.py | 11 +++-- src/textual/_border.py | 2 + src/textual/_segment_tools.py | 73 ++++++++++++++++++++++++++++++++ src/textual/app.py | 27 +++++++++--- src/textual/css/stylesheet.py | 2 +- src/textual/dom.py | 11 +++-- src/textual/message_pump.py | 11 ++++- src/textual/renderables/align.py | 48 +++++++++++++++++++++ src/textual/screen.py | 1 - src/textual/widget.py | 26 +++++++++--- src/textual/widgets/_pretty.py | 21 +++++++-- 11 files changed, 207 insertions(+), 26 deletions(-) create mode 100644 src/textual/renderables/align.py diff --git a/sandbox/will/screens.py b/sandbox/will/screens.py index 6bb425303..750035abb 100644 --- a/sandbox/will/screens.py +++ b/sandbox/will/screens.py @@ -8,7 +8,7 @@ class ModalScreen(Screen): yield Footer() def on_screen_resume(self): - self.query("*").refresh() + self.query_one(Pretty).update(self.app.screen_stack) class NewScreen(Screen): @@ -25,14 +25,19 @@ class ScreenApp(App): ScreenApp Screen { background: #111144; color: white; + + } ScreenApp ModalScreen { background: #114411; color: white; + + } - ScreenApp Static { - height: 100%; + ScreenApp Pretty { + height: auto; content-align: center middle; + background: white 20%; } """ diff --git a/src/textual/_border.py b/src/textual/_border.py index 99dcc0665..516951e37 100644 --- a/src/textual/_border.py +++ b/src/textual/_border.py @@ -263,6 +263,8 @@ class Border: else: render_options = options.update_width(width) + print("LINES", self.renderable) + print(render_options) lines = console.render_lines(self.renderable, render_options) if self.outline: diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py index df9455f8e..d8788df17 100644 --- a/src/textual/_segment_tools.py +++ b/src/textual/_segment_tools.py @@ -10,6 +10,9 @@ from rich.segment import Segment from rich.style import Style from ._cells import cell_len +from ._types import Lines +from .css.types import AlignHorizontal, AlignVertical +from .geometry import Size def line_crop( @@ -124,3 +127,73 @@ def line_pad( Segment(" " * pad_right, style), ] return list(segments) + + +def align_lines( + lines: Lines, + style: Style, + size: Size, + horizontal: AlignHorizontal, + vertical: AlignVertical, +) -> Iterable[list[Segment]]: + """Align lines. + + Args: + lines (Lines): A list of lines. + style (Style): Background style. + size (Size): Size of container. + horizontal (AlignHorizontal): Horizontal alignment. + vertical (AlignVertical): Vertical alignment + + Returns: + Iterable[list[Segment]]: Aligned lines. + + """ + + width, height = size + shape_width, shape_height = Segment.get_shape(lines) + + print("len lines", len(lines)) + print(width, height) + print(shape_width, shape_height) + + def blank_lines(count: int) -> Lines: + return [[Segment(" " * width, style)]] * count + + top_blank_lines = bottom_blank_lines = 0 + vertical_excess_space = max(0, height - shape_height) + print("VERTICAL EXCESS", vertical_excess_space) + print("height", height, "shape height", shape_height) + if vertical == "top": + bottom_blank_lines = vertical_excess_space + elif vertical == "middle": + top_blank_lines = vertical_excess_space // 2 + bottom_blank_lines = height - top_blank_lines + elif vertical == "bottom": + top_blank_lines = vertical_excess_space + + print(top_blank_lines) + yield from blank_lines(top_blank_lines) + + horizontal_excess_space = max(0, width - shape_width) + + adjust_line_length = Segment.adjust_line_length + if horizontal == "left": + for line in lines: + yield adjust_line_length(line, width, style, pad=True) + + elif horizontal == "center": + left_space = horizontal_excess_space // 2 + for line in lines: + yield [ + Segment(" " * left_space, style), + *adjust_line_length(line, width - left_space, style, pad=True), + ] + + elif horizontal == "right": + get_line_length = Segment.get_line_length + for line in lines: + left_space = width - get_line_length(line) + yield [*line, Segment(" " * left_space, style)] + + yield from blank_lines(bottom_blank_lines) diff --git a/src/textual/app.py b/src/textual/app.py index b1f292a19..993b10f4f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -668,7 +668,7 @@ class App(Generic[ReturnType], DOMNode): self.screen.post_message_no_wait(events.ScreenResume(self)) return current_screen - def pop_screen(self) -> Screen: + def pop_screen(self, remove: bool | None = None) -> Screen: """Pop the current screen from the stack, and switch to the previous screen. Returns: @@ -680,10 +680,25 @@ class App(Generic[ReturnType], DOMNode): "Can't pop screen; there must be at least one screen on the stack" ) screen = screen_stack.pop() - screen.remove() screen.post_message_no_wait(events.ScreenSuspend(self)) self.screen._screen_resized(self.size) self.screen.post_message_no_wait(events.ScreenResume(self)) + + if remove is None: + if screen not in self.SCREENS.values(): + screen.remove() + else: + screen.detach() + else: + if remove: + if screen in self.SCREENS.values(): + raise ScreenStackError("Can't remove screen set in App.SCREENS") + screen.remove() + else: + screen.detach() + + print(self._registry) + return screen def set_focus(self, widget: Widget | None) -> None: @@ -692,7 +707,6 @@ class App(Generic[ReturnType], DOMNode): Args: widget (Widget): [description] """ - self.log("set_focus", widget=widget) if widget == self.focused: # Widget is already focused return @@ -910,7 +924,7 @@ class App(Generic[ReturnType], DOMNode): if child not in self._registry: parent.children._append(child) self._registry.add(child) - child.set_parent(parent) + child._attach(parent) child.on_register(self) child.start_messages() return True @@ -948,10 +962,11 @@ class App(Generic[ReturnType], DOMNode): """Unregister a widget. Args: - widget (Widget): _description_ + widget (Widget): A Widget to unregister """ if isinstance(widget._parent, Widget): widget._parent.children._remove(widget) + widget._attach(None) self._registry.discard(widget) async def _disconnect_devtools(self): @@ -964,7 +979,7 @@ class App(Generic[ReturnType], DOMNode): parent (Widget): The parent of the Widget. widget (Widget): The Widget to start. """ - widget.set_parent(parent) + widget._attach(parent) widget.start_messages() widget.post_message_no_wait(events.Mount(sender=parent)) diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index 06957892e..8a0053214 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -355,7 +355,7 @@ class Stylesheet: node._component_styles.clear() for component in node.COMPONENT_CLASSES: virtual_node = DOMNode(classes=component) - virtual_node.set_parent(node) + virtual_node._attach(node) self.apply(virtual_node, animate=False) node._component_styles[component] = virtual_node.styles diff --git a/src/textual/dom.py b/src/textual/dom.py index dab6b494b..0edb69f23 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -445,7 +445,10 @@ class DOMNode(MessagePump): def detach(self) -> None: if self._parent and isinstance(self._parent, DOMNode): self._parent.children._remove(self) - self.set_parent(None) + print(self.parent.children) + self._detach() + print("DETATCH", self) + print(self.app._registry) def get_pseudo_classes(self) -> Iterable[str]: """Get any pseudo classes applicable to this Node, e.g. hover, focus. @@ -472,7 +475,7 @@ class DOMNode(MessagePump): node (DOMNode): A DOM node. """ self.children._append(node) - node.set_parent(self) + node._attach(self) def add_children(self, *nodes: Widget, **named_nodes: Widget) -> None: """Add multiple children to this node. @@ -483,10 +486,10 @@ class DOMNode(MessagePump): """ _append = self.children._append for node in nodes: - node.set_parent(self) + node._attach(self) _append(node) for node_id, node in named_nodes.items(): - node.set_parent(self) + node._attach(self) _append(node) node.id = node_id diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 620dc266e..b263fea83 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -122,9 +122,18 @@ class MessagePump(metaclass=MessagePumpMeta): def log(self, *args, **kwargs) -> None: return self.app.log(*args, **kwargs, _textual_calling_frame=inspect.stack()[1]) - def set_parent(self, parent: MessagePump | None) -> None: + def _attach(self, parent: MessagePump) -> None: + """Set the parent, and therefore attach this node to the tree. + + Args: + parent (MessagePump): Parent node. + """ self._parent = parent + def _detach(self) -> None: + """Unset the parent, removing it from the tree.""" + self._parent = None + def check_message_enabled(self, message: Message) -> bool: return type(message) not in self._disabled_messages diff --git a/src/textual/renderables/align.py b/src/textual/renderables/align.py new file mode 100644 index 000000000..ab5ec8458 --- /dev/null +++ b/src/textual/renderables/align.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from rich.console import Console, ConsoleOptions, RenderableType, RenderResult +from rich.measure import Measurement +from rich.segment import Segment +from rich.style import Style + +from ..geometry import Size +from ..css.types import AlignHorizontal, AlignVertical +from .._segment_tools import align_lines + + +class Align: + def __init__( + self, + renderable: RenderableType, + size: Size, + style: Style, + horizontal: AlignHorizontal, + vertical: AlignVertical, + ) -> None: + self.renderable = renderable + self.size = size + self.style = style + self.horizontal = horizontal + self.vertical = vertical + + def __rich_console__( + self, console: Console, options: ConsoleOptions + ) -> RenderResult: + lines = console.render_lines(self.renderable, options, pad=False) + + new_line = Segment.line() + for line in align_lines( + lines, + self.style, + self.size, + self.horizontal, + self.vertical, + ): + yield from line + yield new_line + + def __rich_measure__( + self, console: "Console", options: "ConsoleOptions" + ) -> Measurement: + width, _ = self.size + return Measurement(width, width) diff --git a/src/textual/screen.py b/src/textual/screen.py index 61c19aa7c..b427b9e89 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -33,7 +33,6 @@ class Screen(Widget): CSS = """ Screen { - layout: vertical; overflow-y: auto; } diff --git a/src/textual/widget.py b/src/textual/widget.py index 51dde4dbb..74a77a4a7 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -14,7 +14,7 @@ from typing import ( ) import rich.repr -from rich.align import Align + from rich.console import Console, RenderableType from rich.measure import Measurement from rich.segment import Segment @@ -27,6 +27,7 @@ from ._animator import BoundAnimator from ._arrange import arrange, DockArrangeResult from ._context import active_app from ._layout import Layout +from ._segment_tools import align_lines from ._styles_cache import StylesCache from ._types import Lines from .box_model import BoxModel, get_box_model @@ -35,6 +36,8 @@ from .geometry import Offset, Region, Size, Spacing, clamp from .layouts.vertical import VerticalLayout from .message import Message from .reactive import Reactive, watch +from .renderables.align import Align + if TYPE_CHECKING: from .app import App, ComposeResult @@ -287,6 +290,7 @@ class Widget(DOMNode): Returns: int: The height of the content. """ + if self.is_container: assert self.layout is not None height = ( @@ -306,7 +310,7 @@ class Widget(DOMNode): renderable = self.render() options = self.console.options.update_width(width).update(highlight=False) - segments = self.console.render(renderable, options) + segments = list(self.console.render(renderable, options)) # Cheaper than counting the lines returned from render_lines! height = sum(text.count("\n") for text, _, _ in segments) self._content_height_cache = (cache_key, height) @@ -989,7 +993,15 @@ class Widget(DOMNode): ) if content_align != ("left", "top"): horizontal, vertical = content_align - renderable = Align(renderable, horizontal, vertical=vertical) + # TODO: This changes the shape of the renderable and breaks alignment + # We need custom functionality that doesn't measure the renderable again + renderable = Align( + renderable, + self.size, + rich_style, + horizontal, + vertical, + ) return renderable @@ -1030,8 +1042,10 @@ class Widget(DOMNode): width, height = self.size renderable = self.render() renderable = self.post_render(renderable) - options = self.console.options.update_dimensions(width, height).update( - highlight=False + options = ( + self.console.options.update_width(width) + .update(highlight=False) + .reset_height() ) lines = self.console.render_lines(renderable, options) self._render_cache = RenderCache(self.size, lines) @@ -1180,9 +1194,9 @@ class Widget(DOMNode): async def on_remove(self, event: events.Remove) -> None: await self.close_messages() - self.app._unregister(self) assert self.parent self.parent.refresh(layout=True) + self.app._unregister(self) def _on_mount(self, event: events.Mount) -> None: widgets = list(self.compose()) diff --git a/src/textual/widgets/_pretty.py b/src/textual/widgets/_pretty.py index d6a406483..44b64ff0b 100644 --- a/src/textual/widgets/_pretty.py +++ b/src/textual/widgets/_pretty.py @@ -2,10 +2,17 @@ from __future__ import annotations from typing import Any from rich.pretty import Pretty as PrettyRenderable -from ._static import Static + +from ..widget import Widget -class Pretty(Static): +class Pretty(Widget): + CSS = """ + Static { + height: auto; + } + """ + def __init__( self, object: Any, @@ -14,10 +21,16 @@ class Pretty(Static): id: str | None = None, classes: str | None = None, ) -> None: - self._object = object super().__init__( - PrettyRenderable(self._object), name=name, id=id, classes=classes, ) + self._renderable = PrettyRenderable(object) + + def render(self) -> PrettyRenderable: + return self._renderable + + def update(self, object: Any) -> None: + self._renderable = PrettyRenderable(object) + self.refresh()