mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
auto height api
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
background: rebeccapurple;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
tint: yellow 50%;
|
||||
}
|
||||
|
||||
#foo:hover {
|
||||
background: greenyellow;
|
||||
}
|
||||
|
||||
@@ -99,23 +99,27 @@ class LayoutUpdate:
|
||||
class SpansUpdate:
|
||||
"""A renderable that applies updated spans to the screen."""
|
||||
|
||||
def __init__(self, spans: list[tuple[int, int, list[Segment]]]) -> None:
|
||||
def __init__(
|
||||
self, spans: list[tuple[int, int, list[Segment]]], crop_y: int
|
||||
) -> None:
|
||||
"""Apply spans, which consist of a tuple of (LINE, OFFSET, SEGMENTS)
|
||||
|
||||
Args:
|
||||
spans (list[tuple[int, int, list[Segment]]]): A list of spans.
|
||||
"""
|
||||
self.spans = spans
|
||||
self.last_y = crop_y - 1
|
||||
|
||||
def __rich_console__(
|
||||
self, console: Console, options: ConsoleOptions
|
||||
) -> RenderResult:
|
||||
move_to = Control.move_to
|
||||
new_line = Segment.line()
|
||||
for last, (y, x, segments) in loop_last(self.spans):
|
||||
last_y = self.last_y
|
||||
for y, x, segments in self.spans:
|
||||
yield move_to(x, y)
|
||||
yield from segments
|
||||
if not last:
|
||||
if y != last_y:
|
||||
yield new_line
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
@@ -318,7 +322,7 @@ class Compositor:
|
||||
|
||||
# Arrange the layout
|
||||
placements, arranged_widgets = widget.layout.arrange(
|
||||
widget, child_region.size, widget.scroll_offset
|
||||
widget, child_region.size
|
||||
)
|
||||
widgets.update(arranged_widgets)
|
||||
placements = sorted(placements, key=get_order)
|
||||
@@ -618,7 +622,7 @@ class Compositor:
|
||||
(y, x1, line_crop(render_lines[y - crop_y], x1, x2))
|
||||
for y, x1, x2 in spans
|
||||
]
|
||||
return SpansUpdate(render_spans)
|
||||
return SpansUpdate(render_spans, crop_y2)
|
||||
|
||||
else:
|
||||
render_lines = self._assemble_chops(chops)
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import sys
|
||||
from typing import ClassVar, NamedTuple, TYPE_CHECKING
|
||||
|
||||
|
||||
from .geometry import Region, Offset, Size
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else: # pragma: no cover
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .widget import Widget
|
||||
|
||||
ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]"
|
||||
|
||||
|
||||
class WidgetPlacement(NamedTuple):
|
||||
"""The position, size, and relative order of a widget within its parent."""
|
||||
@@ -28,41 +36,34 @@ class Layout(ABC):
|
||||
return f"<{self.name}>"
|
||||
|
||||
@abstractmethod
|
||||
def arrange(
|
||||
self, parent: Widget, size: Size, scroll: Offset
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
||||
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||
|
||||
Args:
|
||||
parent (Widget): Parent widget.
|
||||
size (Size): Size of container.
|
||||
scroll (Offset): Offset to apply to the Widget placements.
|
||||
|
||||
Returns:
|
||||
Iterable[WidgetPlacement]: An iterable of widget location
|
||||
"""
|
||||
|
||||
def get_content_width(
|
||||
self, parent: Widget, container_size: Size, viewport_size: Size
|
||||
) -> int:
|
||||
def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int:
|
||||
width: int | None = None
|
||||
for child in parent.displayed_children:
|
||||
for child in widget.displayed_children:
|
||||
assert isinstance(child, Widget)
|
||||
if not child.is_container:
|
||||
child_width = child.get_content_width(container_size, viewport_size)
|
||||
child_width = child.get_content_width(container, viewport)
|
||||
width = child_width if width is None else max(width, child_width)
|
||||
if width is None:
|
||||
width = container_size.width
|
||||
width = container.width
|
||||
return width
|
||||
|
||||
def get_content_height(
|
||||
self, parent: Widget, container_size: Size, viewport_size: Size, width: int
|
||||
self, widget: Widget, container: Size, viewport: Size, width: int
|
||||
) -> int:
|
||||
if not parent.displayed_children:
|
||||
height = container_size.height
|
||||
if not widget.displayed_children:
|
||||
height = container.height
|
||||
else:
|
||||
placements, widgets = self.arrange(
|
||||
parent, Size(width, container_size.height), Offset(0, 0)
|
||||
)
|
||||
placements, widgets = self.arrange(widget, Size(width, container.height))
|
||||
height = max(placement.region.y_max for placement in placements)
|
||||
return height
|
||||
|
||||
@@ -837,19 +837,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
await self.close_messages()
|
||||
|
||||
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
if not self._closed:
|
||||
console = self.console
|
||||
try:
|
||||
if self._sync_available:
|
||||
console.file.write("\x1bP=1s\x1b\\")
|
||||
console.print(self.screen._compositor)
|
||||
if self._sync_available:
|
||||
console.file.write("\x1bP=2s\x1b\\")
|
||||
console.file.flush()
|
||||
except Exception as error:
|
||||
self.on_exception(error)
|
||||
self.display(self.screen._compositor)
|
||||
|
||||
def refresh_css(self, animate: bool = True) -> None:
|
||||
"""Refresh CSS.
|
||||
@@ -864,14 +852,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.refresh(layout=True)
|
||||
|
||||
def display(self, renderable: RenderableType) -> None:
|
||||
if not self._running:
|
||||
return
|
||||
if not self._closed:
|
||||
if self._running and not self._closed:
|
||||
console = self.console
|
||||
if self._sync_available:
|
||||
console.file.write("\x1bP=1s\x1b\\")
|
||||
try:
|
||||
console.print(renderable)
|
||||
console.print(renderable, end="")
|
||||
except Exception as error:
|
||||
self.on_exception(error)
|
||||
if self._sync_available:
|
||||
|
||||
@@ -38,12 +38,13 @@ def get_box_model(
|
||||
is_content_box = styles.box_sizing == "content-box"
|
||||
is_border_box = styles.box_sizing == "border-box"
|
||||
gutter = styles.padding + styles.border.spacing
|
||||
margin = styles.margin
|
||||
|
||||
is_auto_width = styles.width and styles.width.is_auto
|
||||
is_auto_height = styles.height and styles.height.is_auto
|
||||
|
||||
if not has_rule("width"):
|
||||
width = container.width
|
||||
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(
|
||||
@@ -65,7 +66,7 @@ def get_box_model(
|
||||
width = min(width, max_width)
|
||||
|
||||
if not has_rule("height"):
|
||||
height = container.height
|
||||
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
|
||||
@@ -90,9 +91,5 @@ def get_box_model(
|
||||
width += gutter.width
|
||||
height += gutter.height
|
||||
|
||||
size = Size(width, height)
|
||||
margin = styles.margin
|
||||
|
||||
model = BoxModel(size, margin)
|
||||
|
||||
model = BoxModel(Size(width, height), margin)
|
||||
return model
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..css.types import Edge
|
||||
from ..geometry import Offset, Region, Size
|
||||
from .._layout import Layout, WidgetPlacement
|
||||
from .._layout import ArrangeResult, Layout, WidgetPlacement
|
||||
from ..widget import Widget
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
@@ -59,9 +59,7 @@ class DockLayout(Layout):
|
||||
append_dock(Dock(edge, groups[name], z))
|
||||
return docks
|
||||
|
||||
def arrange(
|
||||
self, parent: Widget, size: Size, scroll: Offset
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
||||
|
||||
width, height = size
|
||||
layout_region = Region(0, 0, width, height)
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from typing import cast
|
||||
|
||||
from textual.geometry import Size, Offset, Region
|
||||
from textual._layout import Layout, WidgetPlacement
|
||||
from textual._layout import ArrangeResult, Layout, WidgetPlacement
|
||||
|
||||
from textual.widget import Widget
|
||||
|
||||
@@ -15,9 +15,7 @@ class HorizontalLayout(Layout):
|
||||
|
||||
name = "horizontal"
|
||||
|
||||
def arrange(
|
||||
self, parent: Widget, size: Size, scroll: Offset
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
||||
|
||||
placements: list[WidgetPlacement] = []
|
||||
add_placement = placements.append
|
||||
|
||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import cast, TYPE_CHECKING
|
||||
|
||||
from ..geometry import Offset, Region, Size
|
||||
from .._layout import Layout, WidgetPlacement
|
||||
from ..geometry import Region, Size
|
||||
from .._layout import ArrangeResult, Layout, WidgetPlacement
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..widget import Widget
|
||||
@@ -14,9 +14,7 @@ class VerticalLayout(Layout):
|
||||
|
||||
name = "vertical"
|
||||
|
||||
def arrange(
|
||||
self, parent: Widget, size: Size, scroll: Offset
|
||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
||||
|
||||
placements: list[WidgetPlacement] = []
|
||||
add_placement = placements.append
|
||||
@@ -47,8 +45,6 @@ class VerticalLayout(Layout):
|
||||
y += region.height + margin
|
||||
max_height = y
|
||||
|
||||
# max_height += margins[-1] if margins else 0
|
||||
|
||||
total_region = Region(0, 0, max_width, max_height)
|
||||
add_placement(WidgetPlacement(total_region, None, 0))
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ class Widget(DOMNode):
|
||||
)
|
||||
return box_model
|
||||
|
||||
def get_content_width(self, container_size: Size, viewport_size: Size) -> int:
|
||||
def get_content_width(self, container: Size, viewport: Size) -> int:
|
||||
"""Gets the width of the content area.
|
||||
|
||||
Args:
|
||||
@@ -170,11 +170,11 @@ class Widget(DOMNode):
|
||||
int: The optimal width of the content.
|
||||
"""
|
||||
if self.is_container:
|
||||
return self.layout.get_content_width(self, container_size, viewport_size)
|
||||
return self.layout.get_content_width(self, container, viewport)
|
||||
console = self.app.console
|
||||
renderable = self.render(self.styles.rich_style)
|
||||
measurement = Measurement.get(
|
||||
console, console.options.update_width(container_size.width), renderable
|
||||
console, console.options.update_width(container.width), renderable
|
||||
)
|
||||
width = measurement.maximum
|
||||
return width
|
||||
@@ -203,8 +203,7 @@ class Widget(DOMNode):
|
||||
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!
|
||||
# print(list(segments))
|
||||
# Cheaper than counting the lines returned from render_lines!
|
||||
height = sum(text.count("\n") for text, _, _ in segments)
|
||||
return height
|
||||
|
||||
|
||||
Reference in New Issue
Block a user