auto height api

This commit is contained in:
Will McGugan
2022-05-18 15:30:14 +01:00
parent 61fae791cc
commit d27d22000b
9 changed files with 48 additions and 65 deletions

View File

@@ -3,6 +3,10 @@
background: rebeccapurple;
}
*:focus {
tint: yellow 50%;
}
#foo:hover {
background: greenyellow;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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