mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
alignment fix
This commit is contained in:
@@ -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%;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
48
src/textual/renderables/align.py
Normal file
48
src/textual/renderables/align.py
Normal 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)
|
||||
@@ -33,7 +33,6 @@ class Screen(Widget):
|
||||
|
||||
CSS = """
|
||||
Screen {
|
||||
|
||||
layout: vertical;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user