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; background: rebeccapurple;
} }
*:focus {
tint: yellow 50%;
}
#foo:hover { #foo:hover {
background: greenyellow; background: greenyellow;
} }

View File

@@ -99,23 +99,27 @@ class LayoutUpdate:
class SpansUpdate: class SpansUpdate:
"""A renderable that applies updated spans to the screen.""" """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) """Apply spans, which consist of a tuple of (LINE, OFFSET, SEGMENTS)
Args: Args:
spans (list[tuple[int, int, list[Segment]]]): A list of spans. spans (list[tuple[int, int, list[Segment]]]): A list of spans.
""" """
self.spans = spans self.spans = spans
self.last_y = crop_y - 1
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
move_to = Control.move_to move_to = Control.move_to
new_line = Segment.line() 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 move_to(x, y)
yield from segments yield from segments
if not last: if y != last_y:
yield new_line yield new_line
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
@@ -318,7 +322,7 @@ class Compositor:
# Arrange the layout # Arrange the layout
placements, arranged_widgets = widget.layout.arrange( placements, arranged_widgets = widget.layout.arrange(
widget, child_region.size, widget.scroll_offset widget, child_region.size
) )
widgets.update(arranged_widgets) widgets.update(arranged_widgets)
placements = sorted(placements, key=get_order) placements = sorted(placements, key=get_order)
@@ -618,7 +622,7 @@ class Compositor:
(y, x1, line_crop(render_lines[y - crop_y], x1, x2)) (y, x1, line_crop(render_lines[y - crop_y], x1, x2))
for y, x1, x2 in spans for y, x1, x2 in spans
] ]
return SpansUpdate(render_spans) return SpansUpdate(render_spans, crop_y2)
else: else:
render_lines = self._assemble_chops(chops) render_lines = self._assemble_chops(chops)

View File

@@ -1,15 +1,23 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import sys
from typing import ClassVar, NamedTuple, TYPE_CHECKING from typing import ClassVar, NamedTuple, TYPE_CHECKING
from .geometry import Region, Offset, Size 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: if TYPE_CHECKING:
from .widget import Widget from .widget import Widget
ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]"
class WidgetPlacement(NamedTuple): class WidgetPlacement(NamedTuple):
"""The position, size, and relative order of a widget within its parent.""" """The position, size, and relative order of a widget within its parent."""
@@ -28,41 +36,34 @@ class Layout(ABC):
return f"<{self.name}>" return f"<{self.name}>"
@abstractmethod @abstractmethod
def arrange( def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
self, parent: Widget, size: Size, scroll: Offset
) -> tuple[list[WidgetPlacement], set[Widget]]:
"""Generate a layout map that defines where on the screen the widgets will be drawn. """Generate a layout map that defines where on the screen the widgets will be drawn.
Args: Args:
parent (Widget): Parent widget. parent (Widget): Parent widget.
size (Size): Size of container. size (Size): Size of container.
scroll (Offset): Offset to apply to the Widget placements.
Returns: Returns:
Iterable[WidgetPlacement]: An iterable of widget location Iterable[WidgetPlacement]: An iterable of widget location
""" """
def get_content_width( def get_content_width(self, widget: Widget, container: Size, viewport: Size) -> int:
self, parent: Widget, container_size: Size, viewport_size: Size
) -> int:
width: int | None = None width: int | None = None
for child in parent.displayed_children: for child in widget.displayed_children:
assert isinstance(child, Widget) assert isinstance(child, Widget)
if not child.is_container: 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) width = child_width if width is None else max(width, child_width)
if width is None: if width is None:
width = container_size.width width = container.width
return width return width
def get_content_height( 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: ) -> int:
if not parent.displayed_children: if not widget.displayed_children:
height = container_size.height height = container.height
else: else:
placements, widgets = self.arrange( placements, widgets = self.arrange(widget, Size(width, container.height))
parent, Size(width, container_size.height), Offset(0, 0)
)
height = max(placement.region.y_max for placement in placements) height = max(placement.region.y_max for placement in placements)
return height return height

View File

@@ -837,19 +837,7 @@ class App(Generic[ReturnType], DOMNode):
await self.close_messages() await self.close_messages()
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None: def refresh(self, *, repaint: bool = True, layout: bool = False) -> None:
if not self._running: self.display(self.screen._compositor)
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)
def refresh_css(self, animate: bool = True) -> None: def refresh_css(self, animate: bool = True) -> None:
"""Refresh CSS. """Refresh CSS.
@@ -864,14 +852,12 @@ class App(Generic[ReturnType], DOMNode):
self.refresh(layout=True) self.refresh(layout=True)
def display(self, renderable: RenderableType) -> None: def display(self, renderable: RenderableType) -> None:
if not self._running: if self._running and not self._closed:
return
if not self._closed:
console = self.console console = self.console
if self._sync_available: if self._sync_available:
console.file.write("\x1bP=1s\x1b\\") console.file.write("\x1bP=1s\x1b\\")
try: try:
console.print(renderable) console.print(renderable, end="")
except Exception as error: except Exception as error:
self.on_exception(error) self.on_exception(error)
if self._sync_available: if self._sync_available:

View File

@@ -38,12 +38,13 @@ def get_box_model(
is_content_box = styles.box_sizing == "content-box" is_content_box = styles.box_sizing == "content-box"
is_border_box = styles.box_sizing == "border-box" is_border_box = styles.box_sizing == "border-box"
gutter = styles.padding + styles.border.spacing gutter = styles.padding + styles.border.spacing
margin = styles.margin
is_auto_width = styles.width and styles.width.is_auto is_auto_width = styles.width and styles.width.is_auto
is_auto_height = styles.height and styles.height.is_auto is_auto_height = styles.height and styles.height.is_auto
if not has_rule("width"): if not has_rule("width"):
width = container.width width = container.width - margin.width
elif is_auto_width: elif is_auto_width:
# When width is auto, we want enough space to always fit the content # When width is auto, we want enough space to always fit the content
width = get_content_width( width = get_content_width(
@@ -65,7 +66,7 @@ def get_box_model(
width = min(width, max_width) width = min(width, max_width)
if not has_rule("height"): if not has_rule("height"):
height = container.height height = container.height - margin.height
elif styles.height.is_auto: elif styles.height.is_auto:
height = get_content_height( height = get_content_height(
container - gutter.totals if is_border_box else container, viewport, width container - gutter.totals if is_border_box else container, viewport, width
@@ -90,9 +91,5 @@ def get_box_model(
width += gutter.width width += gutter.width
height += gutter.height height += gutter.height
size = Size(width, height) model = BoxModel(Size(width, height), margin)
margin = styles.margin
model = BoxModel(size, margin)
return model return model

View File

@@ -9,7 +9,7 @@ from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
from .._layout_resolve import layout_resolve from .._layout_resolve import layout_resolve
from ..css.types import Edge from ..css.types import Edge
from ..geometry import Offset, Region, Size from ..geometry import Offset, Region, Size
from .._layout import Layout, WidgetPlacement from .._layout import ArrangeResult, Layout, WidgetPlacement
from ..widget import Widget from ..widget import Widget
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@@ -59,9 +59,7 @@ class DockLayout(Layout):
append_dock(Dock(edge, groups[name], z)) append_dock(Dock(edge, groups[name], z))
return docks return docks
def arrange( def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
self, parent: Widget, size: Size, scroll: Offset
) -> tuple[list[WidgetPlacement], set[Widget]]:
width, height = size width, height = size
layout_region = Region(0, 0, width, height) layout_region = Region(0, 0, width, height)

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import cast from typing import cast
from textual.geometry import Size, Offset, Region 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 from textual.widget import Widget
@@ -15,9 +15,7 @@ class HorizontalLayout(Layout):
name = "horizontal" name = "horizontal"
def arrange( def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
self, parent: Widget, size: Size, scroll: Offset
) -> tuple[list[WidgetPlacement], set[Widget]]:
placements: list[WidgetPlacement] = [] placements: list[WidgetPlacement] = []
add_placement = placements.append add_placement = placements.append

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING
from ..geometry import Offset, Region, Size from ..geometry import Region, Size
from .._layout import Layout, WidgetPlacement from .._layout import ArrangeResult, Layout, WidgetPlacement
if TYPE_CHECKING: if TYPE_CHECKING:
from ..widget import Widget from ..widget import Widget
@@ -14,9 +14,7 @@ class VerticalLayout(Layout):
name = "vertical" name = "vertical"
def arrange( def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
self, parent: Widget, size: Size, scroll: Offset
) -> tuple[list[WidgetPlacement], set[Widget]]:
placements: list[WidgetPlacement] = [] placements: list[WidgetPlacement] = []
add_placement = placements.append add_placement = placements.append
@@ -47,8 +45,6 @@ class VerticalLayout(Layout):
y += region.height + margin y += region.height + margin
max_height = y max_height = y
# max_height += margins[-1] if margins else 0
total_region = Region(0, 0, max_width, max_height) total_region = Region(0, 0, max_width, max_height)
add_placement(WidgetPlacement(total_region, None, 0)) add_placement(WidgetPlacement(total_region, None, 0))

View File

@@ -159,7 +159,7 @@ class Widget(DOMNode):
) )
return box_model 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. """Gets the width of the content area.
Args: Args:
@@ -170,11 +170,11 @@ class Widget(DOMNode):
int: The optimal width of the content. int: The optimal width of the content.
""" """
if self.is_container: 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 console = self.app.console
renderable = self.render(self.styles.rich_style) renderable = self.render(self.styles.rich_style)
measurement = Measurement.get( measurement = Measurement.get(
console, console.options.update_width(container_size.width), renderable console, console.options.update_width(container.width), renderable
) )
width = measurement.maximum width = measurement.maximum
return width return width
@@ -203,8 +203,7 @@ class Widget(DOMNode):
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 = self.console.render(renderable, options)
# # Cheaper than counting the lines returned from render_lines! # Cheaper than counting the lines returned from render_lines!
# print(list(segments))
height = sum(text.count("\n") for text, _, _ in segments) height = sum(text.count("\n") for text, _, _ in segments)
return height return height