diff --git a/sandbox/buttons.css b/sandbox/buttons.css index df19f2b8d..77ff5c379 100644 --- a/sandbox/buttons.css +++ b/sandbox/buttons.css @@ -3,6 +3,10 @@ background: rebeccapurple; } +*:focus { + tint: yellow 50%; +} + #foo:hover { background: greenyellow; } diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 4e054a0db..378d4cd8d 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -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) diff --git a/src/textual/_layout.py b/src/textual/_layout.py index a55164b36..d8930dbe9 100644 --- a/src/textual/_layout.py +++ b/src/textual/_layout.py @@ -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 diff --git a/src/textual/app.py b/src/textual/app.py index f4ee0a5ea..67b43db0f 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -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: diff --git a/src/textual/box_model.py b/src/textual/box_model.py index bbafadfcf..6b3fdf9b6 100644 --- a/src/textual/box_model.py +++ b/src/textual/box_model.py @@ -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 diff --git a/src/textual/layouts/dock.py b/src/textual/layouts/dock.py index f2ff41431..04bd62c9d 100644 --- a/src/textual/layouts/dock.py +++ b/src/textual/layouts/dock.py @@ -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) diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py index c313521c2..35d4defee 100644 --- a/src/textual/layouts/horizontal.py +++ b/src/textual/layouts/horizontal.py @@ -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 diff --git a/src/textual/layouts/vertical.py b/src/textual/layouts/vertical.py index ea987c844..0fb449ef4 100644 --- a/src/textual/layouts/vertical.py +++ b/src/textual/layouts/vertical.py @@ -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)) diff --git a/src/textual/widget.py b/src/textual/widget.py index a45e22d5f..4f367fae7 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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