layers and docks

This commit is contained in:
Will McGugan
2022-07-27 12:22:25 +01:00
parent 6be8fb35fe
commit c98e1b9604
21 changed files with 447 additions and 96 deletions

View File

@@ -15,7 +15,7 @@
App > Screen {
layout: dock;
docks: side=left/1;
background: $surfaceX;
background: $surface;
color: $text-surface;
}
@@ -59,7 +59,7 @@ DataTable {
}
#sidebar .content {
background: $surface;
background: $panel-darken-2;
color: $text-surface;
border-right: wide $background;
content-align: center middle;
@@ -224,7 +224,7 @@ Success {
height:auto;
box-sizing: border-box;
background: $success;
color: $text-success;
color: $text-success-fade-1;
border-top: hkey $success-darken-2;
border-bottom: hkey $success-darken-2;

51
sandbox/will/dock.py Normal file
View File

@@ -0,0 +1,51 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class DockApp(App):
def compose(self) -> ComposeResult:
self.screen.styles.layers = "base sidebar"
header = Static("Header", id="header")
header.styles.dock = "top"
header.styles.height = "3"
header.styles.background = "blue"
header.styles.color = "white"
header.styles.margin = 0
header.styles.align_horizontal = "center"
# header.styles.layer = "base"
header.styles.box_sizing = "border-box"
yield header
footer = Static("Footer")
footer.styles.dock = "bottom"
footer.styles.height = 1
footer.styles.background = "green"
footer.styles.color = "white"
yield footer
sidebar = Static("Sidebar", id="sidebar")
sidebar.styles.dock = "right"
sidebar.styles.width = 20
sidebar.styles.height = "100%"
sidebar.styles.background = "magenta"
# sidebar.styles.layer = "sidebar"
yield sidebar
for n, color in zip(range(5), ["red", "green", "blue", "yellow", "magenta"]):
thing = Static(f"Thing {n}", id=f"#thing{n}")
thing.styles.background = f"{color} 20%"
thing.styles.height = 5
yield thing
app = DockApp()
if __name__ == "__main__":
app.run()

50
sandbox/will/pred.py Normal file
View File

@@ -0,0 +1,50 @@
def partition_will(pred, values):
if not values:
return [], []
if len(values) == 1:
return ([], values) if pred(values[0]) else (values, [])
values = sorted(values, key=pred)
lower = 0
upper = len(values) - 1
index = (lower + upper) // 2
while True:
value = pred(values[index])
if value and not pred(values[index - 1]):
return values[:index], values[index:]
if value:
upper = index
else:
lower = index
index = (lower + upper) // 2
def partition_more_iter(pred, iterable):
"""
Returns a 2-tuple of iterables derived from the input iterable.
The first yields the items that have ``pred(item) == False``.
The second yields the items that have ``pred(item) == True``.
>>> is_odd = lambda x: x % 2 != 0
>>> iterable = range(10)
>>> even_items, odd_items = partition(is_odd, iterable)
>>> list(even_items), list(odd_items)
([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
If *pred* is None, :func:`bool` is used.
>>> iterable = [0, 1, False, True, '', ' ']
>>> false_items, true_items = partition(None, iterable)
>>> list(false_items), list(true_items)
([0, False, ''], [1, True, ' '])
"""
if pred is None:
pred = bool
evaluations = ((pred(x), x) for x in iterable)
t1, t2 = tee(evaluations)
return (
(x for (cond, x) in t1 if not cond),
(x for (cond, x) in t2 if cond),
)

126
src/textual/_arrange.py Normal file
View File

@@ -0,0 +1,126 @@
from __future__ import annotations
from fractions import Fraction
from typing import TYPE_CHECKING
from .geometry import Region, Size, Spacing
from ._layout import ArrangeResult, WidgetPlacement
from ._partition import partition
if TYPE_CHECKING:
from ._layout import ArrangeResult
from .widget import Widget
def arrange(widget: Widget, size: Size, viewport: Size) -> ArrangeResult:
display_children = [child for child in widget.children if child.display]
arrange_widgets: set[Widget] = set()
dock_layers: dict[str, list[Widget]] = {}
for child in display_children:
dock_layers.setdefault(child.styles.layer or "default", []).append(child)
width, height = size
placements: list[WidgetPlacement] = []
add_placement = placements.append
region = size.region
_WidgetPlacement = WidgetPlacement
top_z = 2**32 - 1
scroll_spacing = Spacing()
for widgets in dock_layers.values():
dock_widgets, layout_widgets = partition(
(lambda widget: not widget.styles.dock), widgets
)
arrange_widgets.update(dock_widgets)
top = right = bottom = left = 0
for dock_widget in dock_widgets:
edge = dock_widget.styles.dock
(
widget_width_fraction,
widget_height_fraction,
margin,
) = dock_widget.get_box_model(
size,
viewport,
Fraction(size.height if edge in ("top", "bottom") else size.width),
)
widget_width = int(widget_width_fraction) + margin.width
widget_height = int(widget_height_fraction) + margin.height
align_offset = dock_widget.styles.align_size(
(widget_width, widget_height), size
)
if edge == "bottom":
dock_region = Region(
0, height - widget_height, widget_width, widget_height
)
bottom = max(bottom, dock_region.height)
elif edge == "top":
dock_region = Region(0, 0, widget_width, widget_height)
top = max(top, dock_region.height)
elif edge == "left":
dock_region = Region(0, 0, widget_width, widget_height)
left = max(left, dock_region.width)
elif edge == "right":
dock_region = Region(
width - widget_width, 0, widget_width, widget_height
)
right = max(right, dock_region.width)
dock_region = dock_region.shrink(margin).translate(align_offset)
add_placement(_WidgetPlacement(dock_region, dock_widget, top_z, True))
dock_spacing = Spacing(top, right, bottom, left)
region = size.region.shrink(dock_spacing)
layout_placements, _layout_widgets, spacing = widget.layout.arrange(
widget, layout_widgets, region.size
)
if _layout_widgets:
scroll_spacing = scroll_spacing.grow_maximum(dock_spacing)
arrange_widgets.update(_layout_widgets)
placement_offset = region.offset
if placement_offset:
layout_placements = [
_WidgetPlacement(_region + placement_offset, widget, order, fixed)
for _region, widget, order, fixed in layout_placements
]
placements.extend(layout_placements)
result = ArrangeResult(placements, arrange_widgets, scroll_spacing)
return result
# dock_spacing = Spacing(top, right, bottom, left)
# region = region.shrink(dock_spacing)
# placements, placement_widgets, spacing = widget.layout.arrange(
# widget, layout_widgets, region.size
# )
# dock_spacing += spacing
# placement_offset = region.offset
# if placement_offset:
# placements = [
# _WidgetPlacement(_region + placement_offset, widget, order, fixed)
# for _region, widget, order, fixed in placements
# ]
# return ArrangeResult(
# (dock_placements + placements),
# placement_widgets.union(layout_widgets),
# dock_spacing,
# )

View File

@@ -26,7 +26,7 @@ from rich.segment import Segment
from rich.style import Style
from . import errors
from .geometry import Region, Offset, Size
from .geometry import Region, Offset, Size, Spacing
from ._cells import cell_len
from ._profile import timer
@@ -314,8 +314,7 @@ class Compositor:
root (Widget): Top level widget.
Returns:
map[dict[Widget, RenderRegion], Size]: A mapping of widget on to render region
and the "virtual size" (scrollable region)
tuple[CompositorMap, set[Widget]]: Compositor map and set of widgets.
"""
ORIGIN = Offset(0, 0)
@@ -364,27 +363,43 @@ class Compositor:
if widget.is_container:
# Arrange the layout
placements, arranged_widgets = widget._arrange(child_region.size)
placements, arranged_widgets, spacing = widget._arrange(
child_region.size
)
widgets.update(arranged_widgets)
placements = sorted(placements, key=get_order)
# An offset added to all placements
placement_offset = (
container_region.offset + layout_offset - widget.scroll_offset
)
placement_offset = container_region.offset + layout_offset
placement_scroll_offset = placement_offset - widget.scroll_offset
_layers = widget.layers
layers_to_index = {
layer_name: index for index, layer_name in enumerate(_layers)
}
get_layer_index = layers_to_index.get
# Add all the widgets
for sub_region, sub_widget, z in placements:
for sub_region, sub_widget, z, fixed in placements:
# Combine regions with children to calculate the "virtual size"
total_region = total_region.union(sub_region)
if sub_widget is not None:
add_widget(
sub_widget,
sub_region,
sub_region + placement_offset,
order + (z,),
sub_clip,
)
if fixed:
widget_region = sub_region + placement_offset
else:
total_region = total_region.union(sub_region.grow(spacing))
widget_region = sub_region + placement_scroll_offset
if sub_widget is None:
continue
widget_order = order + (get_layer_index(sub_widget.layer, 0), z)
add_widget(
sub_widget,
sub_region,
widget_region,
widget_order,
sub_clip,
)
# Add any scrollbars
for chrome_widget, chrome_region in widget._arrange_scrollbars(

View File

@@ -5,7 +5,7 @@ import sys
from typing import ClassVar, NamedTuple, TYPE_CHECKING
from .geometry import Region, Size
from .geometry import Region, Size, Spacing
if sys.version_info >= (3, 10):
from typing import TypeAlias
@@ -16,7 +16,13 @@ else: # pragma: no cover
if TYPE_CHECKING:
from .widget import Widget
ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]"
class ArrangeResult(NamedTuple):
"""The result of an arrange operation."""
placements: list[WidgetPlacement]
widgets: set[Widget]
spacing: Spacing = Spacing()
class WidgetPlacement(NamedTuple):
@@ -25,6 +31,7 @@ class WidgetPlacement(NamedTuple):
region: Region
widget: Widget | None = None # A widget of None means empty space
order: int = 0
fixed: bool = False
class Layout(ABC):
@@ -36,7 +43,9 @@ class Layout(ABC):
return f"<{self.name}>"
@abstractmethod
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
"""Generate a layout map that defines where on the screen the widgets will be drawn.
Args:

33
src/textual/_partition.py Normal file
View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from typing import Callable, Iterable, TypeVar
T = TypeVar("T")
def partition(
pred: Callable[[T], bool], iterable: Iterable[T]
) -> tuple[list[T], list[T]]:
"""Partition a sequence in to two list from a given predicate. The first list will contain
the values where the predicate is False, the second list will contain the remaining values.
Args:
pred (Callable[[T], bool]): A callable that returns True or False for a given value.
iterable (Iterable[T]): In Iterable of values.
Returns:
tuple[list[T], list[T]]: A list of values where the predicate is False, and a list
where the predicate is True.
"""
result: tuple[list[T], list[T]] = ([], [])
appends = (result[0].append, result[1].append)
for value in iterable:
appends[pred(value)](value)
return result
if __name__ == "__main__":
print(partition((lambda n: bool(n % 2)), list(range(20))))

View File

@@ -94,7 +94,7 @@ def get_box_model(
else:
# Explicit height set
content_height = styles.height.resolve_dimension(
sizing_container, viewport, fraction_unit
sizing_container - styles.margin.totals, viewport, fraction_unit
)
if is_border_box:
content_height -= gutter.height

View File

@@ -20,6 +20,8 @@ from typing import Callable, NamedTuple
import rich.repr
from rich.color import Color as RichColor
from rich.color import ColorType
from rich.color_triplet import ColorTriplet
from rich.style import Style
from rich.text import Text
@@ -30,6 +32,9 @@ from ._color_constants import COLOR_NAME_TO_RGB
from .geometry import clamp
_TRUECOLOR = ColorType.TRUECOLOR
class HLS(NamedTuple):
"""A color in HLS format."""
@@ -132,11 +137,10 @@ class Color(NamedTuple):
def __rich__(self) -> Text:
"""A Rich method to show the color."""
r, g, b, _ = self
return Text(
f" {self!r} ",
style=Style.from_color(
self.get_contrast_text().rich_color, RichColor.from_rgb(r, g, b)
self.get_contrast_text().rich_color, self.rich_color
),
)
@@ -161,9 +165,10 @@ class Color(NamedTuple):
@property
def rich_color(self) -> RichColor:
"""This color encoded in Rich's Color class."""
# TODO: This isn't cheap as I'd like - cache in a LRUCache ?
r, g, b, _a = self
return RichColor.from_rgb(r, g, b)
return RichColor(
f"#{r:02X}{g:02X}{b:02X}", _TRUECOLOR, None, ColorTriplet(r, g, b)
)
@property
def normalized(self) -> tuple[float, float, float]:
@@ -374,7 +379,7 @@ class Color(NamedTuple):
Returns:
Color: New color.
"""
return self.darken(-amount).clamped
return self.darken(-amount)
@lru_cache(maxsize=1024)
def get_contrast_text(self, alpha=0.95) -> Color:

View File

@@ -503,31 +503,19 @@ def dock_property_help_text(property_name: str, context: StylingContext) -> Help
return HelpText(
summary=f"Invalid value for [i]{property_name}[/] property",
bullets=[
Bullet("The value must be one of the defined docks"),
Bullet("The value must be one of 'top', 'right', 'bottom' or 'left'"),
*ContextSpecificBullets(
inline=[
Bullet(
"Attach a widget to a dock declared on the parent",
examples=[
Example(
f'widget.styles.dock = "left" [dim] # assumes parent widget has declared left dock[/]'
)
],
"The 'dock' rule aligns a widget relative to the screen.",
examples=[Example(f'header.styles.dock = "top"')],
)
],
css=[
Bullet(
"Define a dock using the [i]docks[/] property",
examples=[
Example("docks: [u]lhs[/]=left/2;"),
],
),
Bullet(
"Then attach a widget to a defined dock using the [i]dock[/] property",
examples=[
Example("dock: [scope.key][u]lhs[/][/];"),
],
),
"The 'dock' rule aligns a widget relative to the screen.",
examples=[Example(f"dock: top")],
)
],
).get_by_context(context),
],

View File

@@ -39,6 +39,7 @@ from .scalar import (
Scalar,
ScalarOffset,
ScalarParseError,
percentage_string_to_float,
)
from .transition import Transition
from ..geometry import Spacing, SpacingDimensions, clamp
@@ -47,7 +48,7 @@ if TYPE_CHECKING:
from .._layout import Layout
from .styles import DockGroup, Styles, StylesBase
from .types import EdgeType, AlignHorizontal, AlignVertical
from .types import DockEdge, EdgeType, AlignHorizontal, AlignVertical
BorderDefinition = (
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
@@ -533,7 +534,9 @@ class DockProperty:
the docks themselves, and where they are located on screen.
"""
def __get__(self, obj: StylesBase, objtype: type[StylesBase] | None = None) -> str:
def __get__(
self, obj: StylesBase, objtype: type[StylesBase] | None = None
) -> DockEdge:
"""Get the Dock property
Args:
@@ -543,7 +546,7 @@ class DockProperty:
Returns:
str: The dock name as a string, or "" if the rule is not set.
"""
return obj.get_rule("dock", "_default")
return cast(DockEdge, obj.get_rule("dock", ""))
def __set__(self, obj: Styles, dock_name: str | None):
"""Set the Dock property
@@ -839,15 +842,25 @@ class ColorProperty:
if obj.set_rule(self.name, color):
obj.refresh(children=True)
elif isinstance(color, str):
try:
parsed_color = Color.parse(color)
except ColorParseError as error:
raise StyleValueError(
f"Invalid color value '{color}'",
help_text=color_property_help_text(
self.name, context="inline", error=error
),
)
alpha = 1.0
parsed_color = Color(255, 255, 255)
for token in color.split():
if token.endswith("%"):
try:
alpha = percentage_string_to_float(token)
except ValueError:
raise StyleValueError(f"invalid percentage value '{token}'")
continue
try:
parsed_color = Color.parse(token)
except ColorParseError as error:
raise StyleValueError(
f"Invalid color value '{token}'",
help_text=color_property_help_text(
self.name, context="inline", error=error
),
)
parsed_color = parsed_color.with_alpha(alpha)
if obj.set_rule(self.name, parsed_color):
obj.refresh(children=True)
else:

View File

@@ -619,14 +619,18 @@ class StylesBuilder:
self.styles.text_style = style_definition
def process_dock(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) > 1:
if len(tokens) > 1 or tokens[0].value not in VALID_EDGE:
self.error(
name,
tokens[1],
tokens[0],
dock_property_help_text(name, context="css"),
)
self.styles._rules["dock"] = tokens[0].value if tokens else ""
dock = tokens[0].value
self.styles._rules["dock"] = dock
def process_docks(self, name: str, tokens: list[Token]) -> None:
def docks_error(name, token):

View File

@@ -12,7 +12,7 @@ from rich.style import Style
from .._animator import Animation, EasingFunction
from ..color import Color
from ..geometry import Spacing
from ..geometry import Size, Offset, Spacing
from ._style_properties import (
AlignProperty,
BorderProperty,
@@ -436,6 +436,14 @@ class StylesBase(ABC):
offset_y = parent_height - height
return offset_y
def align_size(self, child: tuple[int, int], parent: tuple[int, int]) -> Offset:
width, height = child
parent_width, parent_height = parent
return Offset(
self.align_width(width, parent_width),
self.align_height(height, parent_height),
)
@rich.repr.auto
@dataclass

View File

@@ -12,6 +12,7 @@ else:
Edge = Literal["top", "right", "bottom", "left"]
DockEdge = Literal["top", "right", "bottom", "left", ""]
EdgeType = Literal[
"",
"ascii",

View File

@@ -347,9 +347,8 @@ class DOMNode(MessagePump):
def text_style(self) -> Style:
"""Get the text style object.
A widget's style is influenced by its parent. For instance if a widgets background has an alpha,
then its parent's background color will show through. Additionally, widgets will inherit their
parent's text style (i.e. bold, italic etc).
A widget's style is influenced by its parent. for instance if a parent is bold, then
the child will also be bold.
Returns:
Style: Rich Style object.
@@ -357,17 +356,18 @@ class DOMNode(MessagePump):
# TODO: Feels like there may be opportunity for caching here.
style = Style()
for node in reversed(self.ancestors):
style += node.styles.text_style
style = sum(
[node.styles.text_style for node in reversed(self.ancestors)], start=Style()
)
return style
@property
def rich_style(self) -> Style:
"""Get a Rich Style object for this DOMNode."""
(_, _), (background, color) = self.colors
style = Style.from_color(color.rich_color, background.rich_color)
style += self.text_style
_, (background, color) = self.colors
style = (
Style.from_color(color.rich_color, background.rich_color) + self.text_style
)
return style
@property
@@ -386,7 +386,7 @@ class DOMNode(MessagePump):
background += styles.background
if styles.has_rule("color"):
base_color = color
color += styles.color
color = styles.color
return (base_background, base_color), (background, color)
@property
@@ -394,9 +394,9 @@ class DOMNode(MessagePump):
"""Get a list of Nodes by tracing ancestors all the way back to App."""
nodes: list[DOMNode] = [self]
add_node = nodes.append
node = self
node: DOMNode = self
while True:
node = node.parent
node = node._parent
if node is None:
break
add_node(node)

View File

@@ -883,5 +883,23 @@ class Spacing(NamedTuple):
)
return NotImplemented
def grow_maximum(self, other: Spacing) -> Spacing:
"""Grow spacing with a maximum.
Args:
other (Spacing): Spacing object.
Returns:
Spacing: New spacing were the values are maximum of the two values.
"""
top, right, bottom, left = self
other_top, other_right, other_bottom, other_left = other
return Spacing(
max(top, other_top),
max(right, other_right),
max(bottom, other_bottom),
max(left, other_left),
)
NULL_OFFSET = Offset(0, 0)

View File

@@ -4,11 +4,11 @@ import sys
from collections import defaultdict
from dataclasses import dataclass
from operator import attrgetter
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
from typing import TYPE_CHECKING, NamedTuple, Sequence
from .._layout_resolve import layout_resolve
from ..css.types import Edge
from ..geometry import Offset, Region, Size
from ..geometry import Region, Size
from .._layout import ArrangeResult, Layout, WidgetPlacement
from ..widget import Widget
@@ -52,9 +52,9 @@ class DockLayout(Layout):
def __repr__(self):
return "<DockLayout>"
def get_docks(self, parent: Widget) -> list[Dock]:
def get_docks(self, parent: Widget, children: list[Widget]) -> list[Dock]:
groups: dict[str, list[Widget]] = defaultdict(list)
for child in parent.displayed_children:
for child in children:
assert isinstance(child, Widget)
groups[child.styles.dock].append(child)
docks: list[Dock] = []
@@ -63,13 +63,15 @@ class DockLayout(Layout):
append_dock(Dock(edge, groups[name], z))
return docks
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
width, height = size
layout_region = Region(0, 0, width, height)
layers: dict[int, Region] = defaultdict(lambda: layout_region)
docks = self.get_docks(parent)
docks = self.get_docks(parent, children)
def make_dock_options(widget: Widget, edge: Edge) -> DockOptions:
styles = widget.styles
@@ -181,4 +183,4 @@ class DockLayout(Layout):
layers[z] = region
return placements, arranged_widgets
return ArrangeResult(placements, arranged_widgets)

View File

@@ -16,7 +16,9 @@ class HorizontalLayout(Layout):
name = "horizontal"
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
placements: list[WidgetPlacement] = []
add_placement = placements.append
@@ -24,7 +26,6 @@ class HorizontalLayout(Layout):
x = max_width = max_height = Fraction(0)
parent_size = parent.outer_size
children = list(parent.children)
styles = [child.styles for child in children if child.styles.width is not None]
total_fraction = sum(
[int(style.width.value) for style in styles if style.width.is_fraction]
@@ -33,7 +34,7 @@ class HorizontalLayout(Layout):
box_models = [
widget.get_box_model(size, parent_size, fraction_unit)
for widget in cast("list[Widget]", parent.children)
for widget in cast("list[Widget]", children)
]
margins = [

View File

@@ -15,14 +15,15 @@ class VerticalLayout(Layout):
name = "vertical"
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
placements: list[WidgetPlacement] = []
add_placement = placements.append
parent_size = parent.outer_size
children = list(parent.children)
styles = [child.styles for child in children if child.styles.height is not None]
total_fraction = sum(
[int(style.height.value) for style in styles if style.height.is_fraction]
@@ -31,7 +32,7 @@ class VerticalLayout(Layout):
box_models = [
widget.get_box_model(size, parent_size, fraction_unit)
for widget in parent.children
for widget in children
]
margins = [
@@ -43,8 +44,7 @@ class VerticalLayout(Layout):
y = Fraction(box_models[0].margin.top if box_models else 0)
displayed_children = cast("list[Widget]", parent.displayed_children)
for widget, box_model, margin in zip(displayed_children, box_models, margins):
for widget, box_model, margin in zip(children, box_models, margins):
content_width, content_height, box_margin = box_model
offset_x = (
widget.styles.align_width(
@@ -60,4 +60,4 @@ class VerticalLayout(Layout):
total_region = Region(0, 0, size.width, int(y))
add_placement(WidgetPlacement(total_region, None, 0))
return placements, set(displayed_children)
return ArrangeResult(placements, set(children))

View File

@@ -10,6 +10,7 @@ from typing import (
Collection,
Iterable,
NamedTuple,
Tuple,
)
import rich.repr
@@ -24,7 +25,7 @@ from rich.text import Text
from . import errors, events, messages
from ._animator import BoundAnimator
from ._border import Border
from ._arrange import arrange
from ._context import active_app
from ._layout import ArrangeResult, Layout
from ._segment_tools import line_crop
@@ -36,8 +37,6 @@ from .geometry import Offset, Region, Size, Spacing, clamp
from .layouts.vertical import VerticalLayout
from .message import Message
from .reactive import Reactive, watch
from .renderables.opacity import Opacity
from .renderables.tint import Tint
if TYPE_CHECKING:
from .app import App, ComposeResult
@@ -116,7 +115,7 @@ class Widget(DOMNode):
self._content_width_cache: tuple[object, int] = (None, 0)
self._content_height_cache: tuple[object, int] = (None, 0)
self._arrangement: ArrangeResult | None = None
self._arrangement: Tuple[ArrangeResult, Spacing] | None = None
self._arrangement_cache_key: tuple[int, Size] = (-1, Size())
self._styles_cache = StylesCache()
@@ -146,14 +145,17 @@ class Widget(DOMNode):
Returns:
ArrangeResult: Widget locations.
"""
arrange_cache_key = (self.children._updates, size)
if (
self._arrangement is not None
and arrange_cache_key == self._arrangement_cache_key
):
return self._arrangement
self._arrangement = self.layout.arrange(self, size)
self._arrangement_cache_key = (self.children._updates, size)
self._arrangement = arrange(self, size, self.screen.size)
return self._arrangement
def _clear_arrangement_cache(self) -> None:
@@ -542,6 +544,25 @@ class Widget(DOMNode):
"""
return self.is_container
@property
def layer(self) -> str:
"""Get the name of this widgets layer."""
return self.styles.layer or "default"
@property
def layers(self) -> tuple[str, ...]:
"""Layers of from parent.
Returns:
tuple[str, ...]: Tuple of layer names.
"""
for node in self.ancestors:
if not isinstance(node, Widget):
break
if node.styles.has_rule("layers"):
return node.styles.layers
return ("default",)
def _set_dirty(self, *regions: Region) -> None:
"""Set the Widget as 'dirty' (requiring re-paint).

View File

@@ -7,6 +7,12 @@ from ..widget import Widget
class Static(Widget):
CSS = """
Static {
height: auto;
}
"""
def __init__(
self,
renderable: RenderableType = "",