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()
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%;
}
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 = """
Screen {
layout: vertical;
overflow-y: auto;
}

View File

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

View File

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