mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
layers and docks
This commit is contained in:
@@ -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
51
sandbox/will/dock.py
Normal 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
50
sandbox/will/pred.py
Normal 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
126
src/textual/_arrange.py
Normal 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,
|
||||
# )
|
||||
@@ -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(
|
||||
|
||||
@@ -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
33
src/textual/_partition.py
Normal 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))))
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,6 +12,7 @@ else:
|
||||
|
||||
|
||||
Edge = Literal["top", "right", "bottom", "left"]
|
||||
DockEdge = Literal["top", "right", "bottom", "left", ""]
|
||||
EdgeType = Literal[
|
||||
"",
|
||||
"ascii",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ from ..widget import Widget
|
||||
|
||||
|
||||
class Static(Widget):
|
||||
CSS = """
|
||||
Static {
|
||||
height: auto;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
renderable: RenderableType = "",
|
||||
|
||||
Reference in New Issue
Block a user