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;
|
background: rebeccapurple;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus {
|
||||||
|
tint: yellow 50%;
|
||||||
|
}
|
||||||
|
|
||||||
#foo:hover {
|
#foo:hover {
|
||||||
background: greenyellow;
|
background: greenyellow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user