alignment fix

This commit is contained in:
Will McGugan
2022-08-16 17:06:33 +01:00
parent a9fb78edc1
commit 62f7ed8358
11 changed files with 207 additions and 26 deletions

View File

@@ -8,7 +8,7 @@ class ModalScreen(Screen):
yield Footer() yield Footer()
def on_screen_resume(self): def on_screen_resume(self):
self.query("*").refresh() self.query_one(Pretty).update(self.app.screen_stack)
class NewScreen(Screen): class NewScreen(Screen):
@@ -25,14 +25,19 @@ class ScreenApp(App):
ScreenApp Screen { ScreenApp Screen {
background: #111144; background: #111144;
color: white; color: white;
} }
ScreenApp ModalScreen { ScreenApp ModalScreen {
background: #114411; background: #114411;
color: white; color: white;
} }
ScreenApp Static { ScreenApp Pretty {
height: 100%; height: auto;
content-align: center middle; content-align: center middle;
background: white 20%;
} }
""" """

View File

@@ -263,6 +263,8 @@ class Border:
else: else:
render_options = options.update_width(width) render_options = options.update_width(width)
print("LINES", self.renderable)
print(render_options)
lines = console.render_lines(self.renderable, render_options) lines = console.render_lines(self.renderable, render_options)
if self.outline: if self.outline:

View File

@@ -10,6 +10,9 @@ from rich.segment import Segment
from rich.style import Style from rich.style import Style
from ._cells import cell_len from ._cells import cell_len
from ._types import Lines
from .css.types import AlignHorizontal, AlignVertical
from .geometry import Size
def line_crop( def line_crop(
@@ -124,3 +127,73 @@ def line_pad(
Segment(" " * pad_right, style), Segment(" " * pad_right, style),
] ]
return list(segments) 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)

View File

@@ -668,7 +668,7 @@ class App(Generic[ReturnType], DOMNode):
self.screen.post_message_no_wait(events.ScreenResume(self)) self.screen.post_message_no_wait(events.ScreenResume(self))
return current_screen 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. """Pop the current screen from the stack, and switch to the previous screen.
Returns: Returns:
@@ -680,10 +680,25 @@ class App(Generic[ReturnType], DOMNode):
"Can't pop screen; there must be at least one screen on the stack" "Can't pop screen; there must be at least one screen on the stack"
) )
screen = screen_stack.pop() screen = screen_stack.pop()
screen.remove()
screen.post_message_no_wait(events.ScreenSuspend(self)) screen.post_message_no_wait(events.ScreenSuspend(self))
self.screen._screen_resized(self.size) self.screen._screen_resized(self.size)
self.screen.post_message_no_wait(events.ScreenResume(self)) 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 return screen
def set_focus(self, widget: Widget | None) -> None: def set_focus(self, widget: Widget | None) -> None:
@@ -692,7 +707,6 @@ class App(Generic[ReturnType], DOMNode):
Args: Args:
widget (Widget): [description] widget (Widget): [description]
""" """
self.log("set_focus", widget=widget)
if widget == self.focused: if widget == self.focused:
# Widget is already focused # Widget is already focused
return return
@@ -910,7 +924,7 @@ class App(Generic[ReturnType], DOMNode):
if child not in self._registry: if child not in self._registry:
parent.children._append(child) parent.children._append(child)
self._registry.add(child) self._registry.add(child)
child.set_parent(parent) child._attach(parent)
child.on_register(self) child.on_register(self)
child.start_messages() child.start_messages()
return True return True
@@ -948,10 +962,11 @@ class App(Generic[ReturnType], DOMNode):
"""Unregister a widget. """Unregister a widget.
Args: Args:
widget (Widget): _description_ widget (Widget): A Widget to unregister
""" """
if isinstance(widget._parent, Widget): if isinstance(widget._parent, Widget):
widget._parent.children._remove(widget) widget._parent.children._remove(widget)
widget._attach(None)
self._registry.discard(widget) self._registry.discard(widget)
async def _disconnect_devtools(self): async def _disconnect_devtools(self):
@@ -964,7 +979,7 @@ class App(Generic[ReturnType], DOMNode):
parent (Widget): The parent of the Widget. parent (Widget): The parent of the Widget.
widget (Widget): The Widget to start. widget (Widget): The Widget to start.
""" """
widget.set_parent(parent) widget._attach(parent)
widget.start_messages() widget.start_messages()
widget.post_message_no_wait(events.Mount(sender=parent)) widget.post_message_no_wait(events.Mount(sender=parent))

View File

@@ -355,7 +355,7 @@ class Stylesheet:
node._component_styles.clear() node._component_styles.clear()
for component in node.COMPONENT_CLASSES: for component in node.COMPONENT_CLASSES:
virtual_node = DOMNode(classes=component) virtual_node = DOMNode(classes=component)
virtual_node.set_parent(node) virtual_node._attach(node)
self.apply(virtual_node, animate=False) self.apply(virtual_node, animate=False)
node._component_styles[component] = virtual_node.styles node._component_styles[component] = virtual_node.styles

View File

@@ -445,7 +445,10 @@ class DOMNode(MessagePump):
def detach(self) -> None: def detach(self) -> None:
if self._parent and isinstance(self._parent, DOMNode): if self._parent and isinstance(self._parent, DOMNode):
self._parent.children._remove(self) 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]: def get_pseudo_classes(self) -> Iterable[str]:
"""Get any pseudo classes applicable to this Node, e.g. hover, focus. """Get any pseudo classes applicable to this Node, e.g. hover, focus.
@@ -472,7 +475,7 @@ class DOMNode(MessagePump):
node (DOMNode): A DOM node. node (DOMNode): A DOM node.
""" """
self.children._append(node) self.children._append(node)
node.set_parent(self) node._attach(self)
def add_children(self, *nodes: Widget, **named_nodes: Widget) -> None: def add_children(self, *nodes: Widget, **named_nodes: Widget) -> None:
"""Add multiple children to this node. """Add multiple children to this node.
@@ -483,10 +486,10 @@ class DOMNode(MessagePump):
""" """
_append = self.children._append _append = self.children._append
for node in nodes: for node in nodes:
node.set_parent(self) node._attach(self)
_append(node) _append(node)
for node_id, node in named_nodes.items(): for node_id, node in named_nodes.items():
node.set_parent(self) node._attach(self)
_append(node) _append(node)
node.id = node_id node.id = node_id

View File

@@ -122,9 +122,18 @@ class MessagePump(metaclass=MessagePumpMeta):
def log(self, *args, **kwargs) -> None: def log(self, *args, **kwargs) -> None:
return self.app.log(*args, **kwargs, _textual_calling_frame=inspect.stack()[1]) 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 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: def check_message_enabled(self, message: Message) -> bool:
return type(message) not in self._disabled_messages return type(message) not in self._disabled_messages

View File

@@ -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)

View File

@@ -33,7 +33,6 @@ class Screen(Widget):
CSS = """ CSS = """
Screen { Screen {
layout: vertical; layout: vertical;
overflow-y: auto; overflow-y: auto;
} }

View File

@@ -14,7 +14,7 @@ from typing import (
) )
import rich.repr import rich.repr
from rich.align import Align
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
from rich.measure import Measurement from rich.measure import Measurement
from rich.segment import Segment from rich.segment import Segment
@@ -27,6 +27,7 @@ from ._animator import BoundAnimator
from ._arrange import arrange, DockArrangeResult from ._arrange import arrange, DockArrangeResult
from ._context import active_app from ._context import active_app
from ._layout import Layout from ._layout import Layout
from ._segment_tools import align_lines
from ._styles_cache import StylesCache from ._styles_cache import StylesCache
from ._types import Lines from ._types import Lines
from .box_model import BoxModel, get_box_model 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 .layouts.vertical import VerticalLayout
from .message import Message from .message import Message
from .reactive import Reactive, watch from .reactive import Reactive, watch
from .renderables.align import Align
if TYPE_CHECKING: if TYPE_CHECKING:
from .app import App, ComposeResult from .app import App, ComposeResult
@@ -287,6 +290,7 @@ class Widget(DOMNode):
Returns: Returns:
int: The height of the content. int: The height of the content.
""" """
if self.is_container: if self.is_container:
assert self.layout is not None assert self.layout is not None
height = ( height = (
@@ -306,7 +310,7 @@ class Widget(DOMNode):
renderable = self.render() renderable = self.render()
options = self.console.options.update_width(width).update(highlight=False) 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! # Cheaper than counting the lines returned from render_lines!
height = sum(text.count("\n") for text, _, _ in segments) height = sum(text.count("\n") for text, _, _ in segments)
self._content_height_cache = (cache_key, height) self._content_height_cache = (cache_key, height)
@@ -989,7 +993,15 @@ class Widget(DOMNode):
) )
if content_align != ("left", "top"): if content_align != ("left", "top"):
horizontal, vertical = content_align 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 return renderable
@@ -1030,8 +1042,10 @@ class Widget(DOMNode):
width, height = self.size width, height = self.size
renderable = self.render() renderable = self.render()
renderable = self.post_render(renderable) renderable = self.post_render(renderable)
options = self.console.options.update_dimensions(width, height).update( options = (
highlight=False self.console.options.update_width(width)
.update(highlight=False)
.reset_height()
) )
lines = self.console.render_lines(renderable, options) lines = self.console.render_lines(renderable, options)
self._render_cache = RenderCache(self.size, lines) self._render_cache = RenderCache(self.size, lines)
@@ -1180,9 +1194,9 @@ class Widget(DOMNode):
async def on_remove(self, event: events.Remove) -> None: async def on_remove(self, event: events.Remove) -> None:
await self.close_messages() await self.close_messages()
self.app._unregister(self)
assert self.parent assert self.parent
self.parent.refresh(layout=True) self.parent.refresh(layout=True)
self.app._unregister(self)
def _on_mount(self, event: events.Mount) -> None: def _on_mount(self, event: events.Mount) -> None:
widgets = list(self.compose()) widgets = list(self.compose())

View File

@@ -2,10 +2,17 @@ from __future__ import annotations
from typing import Any from typing import Any
from rich.pretty import Pretty as PrettyRenderable 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__( def __init__(
self, self,
object: Any, object: Any,
@@ -14,10 +21,16 @@ class Pretty(Static):
id: str | None = None, id: str | None = None,
classes: str | None = None, classes: str | None = None,
) -> None: ) -> None:
self._object = object
super().__init__( super().__init__(
PrettyRenderable(self._object),
name=name, name=name,
id=id, id=id,
classes=classes, 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()