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 {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
background: $surfaceX;
|
background: $surface;
|
||||||
color: $text-surface;
|
color: $text-surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ DataTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
background: $surface;
|
background: $panel-darken-2;
|
||||||
color: $text-surface;
|
color: $text-surface;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
@@ -224,7 +224,7 @@ Success {
|
|||||||
height:auto;
|
height:auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: $success;
|
background: $success;
|
||||||
color: $text-success;
|
color: $text-success-fade-1;
|
||||||
|
|
||||||
border-top: hkey $success-darken-2;
|
border-top: hkey $success-darken-2;
|
||||||
border-bottom: 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 rich.style import Style
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from .geometry import Region, Offset, Size
|
from .geometry import Region, Offset, Size, Spacing
|
||||||
|
|
||||||
from ._cells import cell_len
|
from ._cells import cell_len
|
||||||
from ._profile import timer
|
from ._profile import timer
|
||||||
@@ -314,8 +314,7 @@ class Compositor:
|
|||||||
root (Widget): Top level widget.
|
root (Widget): Top level widget.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
map[dict[Widget, RenderRegion], Size]: A mapping of widget on to render region
|
tuple[CompositorMap, set[Widget]]: Compositor map and set of widgets.
|
||||||
and the "virtual size" (scrollable region)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ORIGIN = Offset(0, 0)
|
ORIGIN = Offset(0, 0)
|
||||||
@@ -364,27 +363,43 @@ class Compositor:
|
|||||||
|
|
||||||
if widget.is_container:
|
if widget.is_container:
|
||||||
# Arrange the layout
|
# Arrange the layout
|
||||||
placements, arranged_widgets = widget._arrange(child_region.size)
|
placements, arranged_widgets, spacing = widget._arrange(
|
||||||
|
child_region.size
|
||||||
|
)
|
||||||
widgets.update(arranged_widgets)
|
widgets.update(arranged_widgets)
|
||||||
placements = sorted(placements, key=get_order)
|
placements = sorted(placements, key=get_order)
|
||||||
|
|
||||||
# An offset added to all placements
|
# An offset added to all placements
|
||||||
placement_offset = (
|
placement_offset = container_region.offset + layout_offset
|
||||||
container_region.offset + layout_offset - widget.scroll_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
|
# 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"
|
# Combine regions with children to calculate the "virtual size"
|
||||||
total_region = total_region.union(sub_region)
|
if fixed:
|
||||||
if sub_widget is not None:
|
widget_region = sub_region + placement_offset
|
||||||
add_widget(
|
else:
|
||||||
sub_widget,
|
total_region = total_region.union(sub_region.grow(spacing))
|
||||||
sub_region,
|
widget_region = sub_region + placement_scroll_offset
|
||||||
sub_region + placement_offset,
|
|
||||||
order + (z,),
|
if sub_widget is None:
|
||||||
sub_clip,
|
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
|
# Add any scrollbars
|
||||||
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import sys
|
|||||||
from typing import ClassVar, NamedTuple, TYPE_CHECKING
|
from typing import ClassVar, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
from .geometry import Region, Size
|
from .geometry import Region, Size, Spacing
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
if sys.version_info >= (3, 10):
|
||||||
from typing import TypeAlias
|
from typing import TypeAlias
|
||||||
@@ -16,7 +16,13 @@ else: # pragma: no cover
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .widget import Widget
|
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):
|
class WidgetPlacement(NamedTuple):
|
||||||
@@ -25,6 +31,7 @@ class WidgetPlacement(NamedTuple):
|
|||||||
region: Region
|
region: Region
|
||||||
widget: Widget | None = None # A widget of None means empty space
|
widget: Widget | None = None # A widget of None means empty space
|
||||||
order: int = 0
|
order: int = 0
|
||||||
|
fixed: bool = False
|
||||||
|
|
||||||
|
|
||||||
class Layout(ABC):
|
class Layout(ABC):
|
||||||
@@ -36,7 +43,9 @@ class Layout(ABC):
|
|||||||
return f"<{self.name}>"
|
return f"<{self.name}>"
|
||||||
|
|
||||||
@abstractmethod
|
@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.
|
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||||
|
|
||||||
Args:
|
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:
|
else:
|
||||||
# Explicit height set
|
# Explicit height set
|
||||||
content_height = styles.height.resolve_dimension(
|
content_height = styles.height.resolve_dimension(
|
||||||
sizing_container, viewport, fraction_unit
|
sizing_container - styles.margin.totals, viewport, fraction_unit
|
||||||
)
|
)
|
||||||
if is_border_box:
|
if is_border_box:
|
||||||
content_height -= gutter.height
|
content_height -= gutter.height
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from typing import Callable, NamedTuple
|
|||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.color import Color as RichColor
|
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.style import Style
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
@@ -30,6 +32,9 @@ from ._color_constants import COLOR_NAME_TO_RGB
|
|||||||
from .geometry import clamp
|
from .geometry import clamp
|
||||||
|
|
||||||
|
|
||||||
|
_TRUECOLOR = ColorType.TRUECOLOR
|
||||||
|
|
||||||
|
|
||||||
class HLS(NamedTuple):
|
class HLS(NamedTuple):
|
||||||
"""A color in HLS format."""
|
"""A color in HLS format."""
|
||||||
|
|
||||||
@@ -132,11 +137,10 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
def __rich__(self) -> Text:
|
def __rich__(self) -> Text:
|
||||||
"""A Rich method to show the color."""
|
"""A Rich method to show the color."""
|
||||||
r, g, b, _ = self
|
|
||||||
return Text(
|
return Text(
|
||||||
f" {self!r} ",
|
f" {self!r} ",
|
||||||
style=Style.from_color(
|
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
|
@property
|
||||||
def rich_color(self) -> RichColor:
|
def rich_color(self) -> RichColor:
|
||||||
"""This color encoded in Rich's Color class."""
|
"""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
|
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
|
@property
|
||||||
def normalized(self) -> tuple[float, float, float]:
|
def normalized(self) -> tuple[float, float, float]:
|
||||||
@@ -374,7 +379,7 @@ class Color(NamedTuple):
|
|||||||
Returns:
|
Returns:
|
||||||
Color: New color.
|
Color: New color.
|
||||||
"""
|
"""
|
||||||
return self.darken(-amount).clamped
|
return self.darken(-amount)
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def get_contrast_text(self, alpha=0.95) -> Color:
|
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(
|
return HelpText(
|
||||||
summary=f"Invalid value for [i]{property_name}[/] property",
|
summary=f"Invalid value for [i]{property_name}[/] property",
|
||||||
bullets=[
|
bullets=[
|
||||||
Bullet("The value must be one of the defined docks"),
|
Bullet("The value must be one of 'top', 'right', 'bottom' or 'left'"),
|
||||||
*ContextSpecificBullets(
|
*ContextSpecificBullets(
|
||||||
inline=[
|
inline=[
|
||||||
Bullet(
|
Bullet(
|
||||||
"Attach a widget to a dock declared on the parent",
|
"The 'dock' rule aligns a widget relative to the screen.",
|
||||||
examples=[
|
examples=[Example(f'header.styles.dock = "top"')],
|
||||||
Example(
|
|
||||||
f'widget.styles.dock = "left" [dim] # assumes parent widget has declared left dock[/]'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
css=[
|
css=[
|
||||||
Bullet(
|
Bullet(
|
||||||
"Define a dock using the [i]docks[/] property",
|
"The 'dock' rule aligns a widget relative to the screen.",
|
||||||
examples=[
|
examples=[Example(f"dock: top")],
|
||||||
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[/][/];"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).get_by_context(context),
|
).get_by_context(context),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from .scalar import (
|
|||||||
Scalar,
|
Scalar,
|
||||||
ScalarOffset,
|
ScalarOffset,
|
||||||
ScalarParseError,
|
ScalarParseError,
|
||||||
|
percentage_string_to_float,
|
||||||
)
|
)
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||||
@@ -47,7 +48,7 @@ if TYPE_CHECKING:
|
|||||||
from .._layout import Layout
|
from .._layout import Layout
|
||||||
from .styles import DockGroup, Styles, StylesBase
|
from .styles import DockGroup, Styles, StylesBase
|
||||||
|
|
||||||
from .types import EdgeType, AlignHorizontal, AlignVertical
|
from .types import DockEdge, EdgeType, AlignHorizontal, AlignVertical
|
||||||
|
|
||||||
BorderDefinition = (
|
BorderDefinition = (
|
||||||
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
|
"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.
|
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
|
"""Get the Dock property
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -543,7 +546,7 @@ class DockProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
str: The dock name as a string, or "" if the rule is not set.
|
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):
|
def __set__(self, obj: Styles, dock_name: str | None):
|
||||||
"""Set the Dock property
|
"""Set the Dock property
|
||||||
@@ -839,15 +842,25 @@ class ColorProperty:
|
|||||||
if obj.set_rule(self.name, color):
|
if obj.set_rule(self.name, color):
|
||||||
obj.refresh(children=True)
|
obj.refresh(children=True)
|
||||||
elif isinstance(color, str):
|
elif isinstance(color, str):
|
||||||
try:
|
alpha = 1.0
|
||||||
parsed_color = Color.parse(color)
|
parsed_color = Color(255, 255, 255)
|
||||||
except ColorParseError as error:
|
for token in color.split():
|
||||||
raise StyleValueError(
|
if token.endswith("%"):
|
||||||
f"Invalid color value '{color}'",
|
try:
|
||||||
help_text=color_property_help_text(
|
alpha = percentage_string_to_float(token)
|
||||||
self.name, context="inline", error=error
|
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):
|
if obj.set_rule(self.name, parsed_color):
|
||||||
obj.refresh(children=True)
|
obj.refresh(children=True)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -619,14 +619,18 @@ class StylesBuilder:
|
|||||||
self.styles.text_style = style_definition
|
self.styles.text_style = style_definition
|
||||||
|
|
||||||
def process_dock(self, name: str, tokens: list[Token]) -> None:
|
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(
|
self.error(
|
||||||
name,
|
name,
|
||||||
tokens[1],
|
tokens[0],
|
||||||
dock_property_help_text(name, context="css"),
|
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 process_docks(self, name: str, tokens: list[Token]) -> None:
|
||||||
def docks_error(name, token):
|
def docks_error(name, token):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from rich.style import Style
|
|||||||
|
|
||||||
from .._animator import Animation, EasingFunction
|
from .._animator import Animation, EasingFunction
|
||||||
from ..color import Color
|
from ..color import Color
|
||||||
from ..geometry import Spacing
|
from ..geometry import Size, Offset, Spacing
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
AlignProperty,
|
AlignProperty,
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
@@ -436,6 +436,14 @@ class StylesBase(ABC):
|
|||||||
offset_y = parent_height - height
|
offset_y = parent_height - height
|
||||||
return offset_y
|
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
|
@rich.repr.auto
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ else:
|
|||||||
|
|
||||||
|
|
||||||
Edge = Literal["top", "right", "bottom", "left"]
|
Edge = Literal["top", "right", "bottom", "left"]
|
||||||
|
DockEdge = Literal["top", "right", "bottom", "left", ""]
|
||||||
EdgeType = Literal[
|
EdgeType = Literal[
|
||||||
"",
|
"",
|
||||||
"ascii",
|
"ascii",
|
||||||
|
|||||||
@@ -347,9 +347,8 @@ class DOMNode(MessagePump):
|
|||||||
def text_style(self) -> Style:
|
def text_style(self) -> Style:
|
||||||
"""Get the text style object.
|
"""Get the text style object.
|
||||||
|
|
||||||
A widget's style is influenced by its parent. For instance if a widgets background has an alpha,
|
A widget's style is influenced by its parent. for instance if a parent is bold, then
|
||||||
then its parent's background color will show through. Additionally, widgets will inherit their
|
the child will also be bold.
|
||||||
parent's text style (i.e. bold, italic etc).
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Style: Rich Style object.
|
Style: Rich Style object.
|
||||||
@@ -357,17 +356,18 @@ class DOMNode(MessagePump):
|
|||||||
|
|
||||||
# TODO: Feels like there may be opportunity for caching here.
|
# TODO: Feels like there may be opportunity for caching here.
|
||||||
|
|
||||||
style = Style()
|
style = sum(
|
||||||
for node in reversed(self.ancestors):
|
[node.styles.text_style for node in reversed(self.ancestors)], start=Style()
|
||||||
style += node.styles.text_style
|
)
|
||||||
return style
|
return style
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rich_style(self) -> Style:
|
def rich_style(self) -> Style:
|
||||||
"""Get a Rich Style object for this DOMNode."""
|
"""Get a Rich Style object for this DOMNode."""
|
||||||
(_, _), (background, color) = self.colors
|
_, (background, color) = self.colors
|
||||||
style = Style.from_color(color.rich_color, background.rich_color)
|
style = (
|
||||||
style += self.text_style
|
Style.from_color(color.rich_color, background.rich_color) + self.text_style
|
||||||
|
)
|
||||||
return style
|
return style
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -386,7 +386,7 @@ class DOMNode(MessagePump):
|
|||||||
background += styles.background
|
background += styles.background
|
||||||
if styles.has_rule("color"):
|
if styles.has_rule("color"):
|
||||||
base_color = color
|
base_color = color
|
||||||
color += styles.color
|
color = styles.color
|
||||||
return (base_background, base_color), (background, color)
|
return (base_background, base_color), (background, color)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -394,9 +394,9 @@ class DOMNode(MessagePump):
|
|||||||
"""Get a list of Nodes by tracing ancestors all the way back to App."""
|
"""Get a list of Nodes by tracing ancestors all the way back to App."""
|
||||||
nodes: list[DOMNode] = [self]
|
nodes: list[DOMNode] = [self]
|
||||||
add_node = nodes.append
|
add_node = nodes.append
|
||||||
node = self
|
node: DOMNode = self
|
||||||
while True:
|
while True:
|
||||||
node = node.parent
|
node = node._parent
|
||||||
if node is None:
|
if node is None:
|
||||||
break
|
break
|
||||||
add_node(node)
|
add_node(node)
|
||||||
|
|||||||
@@ -883,5 +883,23 @@ class Spacing(NamedTuple):
|
|||||||
)
|
)
|
||||||
return NotImplemented
|
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)
|
NULL_OFFSET = Offset(0, 0)
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import sys
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import attrgetter
|
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 .._layout_resolve import layout_resolve
|
||||||
from ..css.types import Edge
|
from ..css.types import Edge
|
||||||
from ..geometry import Offset, Region, Size
|
from ..geometry import Region, Size
|
||||||
from .._layout import ArrangeResult, Layout, WidgetPlacement
|
from .._layout import ArrangeResult, Layout, WidgetPlacement
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ class DockLayout(Layout):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<DockLayout>"
|
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)
|
groups: dict[str, list[Widget]] = defaultdict(list)
|
||||||
for child in parent.displayed_children:
|
for child in children:
|
||||||
assert isinstance(child, Widget)
|
assert isinstance(child, Widget)
|
||||||
groups[child.styles.dock].append(child)
|
groups[child.styles.dock].append(child)
|
||||||
docks: list[Dock] = []
|
docks: list[Dock] = []
|
||||||
@@ -63,13 +63,15 @@ class DockLayout(Layout):
|
|||||||
append_dock(Dock(edge, groups[name], z))
|
append_dock(Dock(edge, groups[name], z))
|
||||||
return docks
|
return docks
|
||||||
|
|
||||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
def arrange(
|
||||||
|
self, parent: Widget, children: list[Widget], size: Size
|
||||||
|
) -> ArrangeResult:
|
||||||
|
|
||||||
width, height = size
|
width, height = size
|
||||||
layout_region = Region(0, 0, width, height)
|
layout_region = Region(0, 0, width, height)
|
||||||
layers: dict[int, Region] = defaultdict(lambda: layout_region)
|
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:
|
def make_dock_options(widget: Widget, edge: Edge) -> DockOptions:
|
||||||
styles = widget.styles
|
styles = widget.styles
|
||||||
@@ -181,4 +183,4 @@ class DockLayout(Layout):
|
|||||||
|
|
||||||
layers[z] = region
|
layers[z] = region
|
||||||
|
|
||||||
return placements, arranged_widgets
|
return ArrangeResult(placements, arranged_widgets)
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ class HorizontalLayout(Layout):
|
|||||||
|
|
||||||
name = "horizontal"
|
name = "horizontal"
|
||||||
|
|
||||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
def arrange(
|
||||||
|
self, parent: Widget, children: list[Widget], size: Size
|
||||||
|
) -> ArrangeResult:
|
||||||
|
|
||||||
placements: list[WidgetPlacement] = []
|
placements: list[WidgetPlacement] = []
|
||||||
add_placement = placements.append
|
add_placement = placements.append
|
||||||
@@ -24,7 +26,6 @@ class HorizontalLayout(Layout):
|
|||||||
x = max_width = max_height = Fraction(0)
|
x = max_width = max_height = Fraction(0)
|
||||||
parent_size = parent.outer_size
|
parent_size = parent.outer_size
|
||||||
|
|
||||||
children = list(parent.children)
|
|
||||||
styles = [child.styles for child in children if child.styles.width is not None]
|
styles = [child.styles for child in children if child.styles.width is not None]
|
||||||
total_fraction = sum(
|
total_fraction = sum(
|
||||||
[int(style.width.value) for style in styles if style.width.is_fraction]
|
[int(style.width.value) for style in styles if style.width.is_fraction]
|
||||||
@@ -33,7 +34,7 @@ class HorizontalLayout(Layout):
|
|||||||
|
|
||||||
box_models = [
|
box_models = [
|
||||||
widget.get_box_model(size, parent_size, fraction_unit)
|
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 = [
|
margins = [
|
||||||
|
|||||||
@@ -15,14 +15,15 @@ class VerticalLayout(Layout):
|
|||||||
|
|
||||||
name = "vertical"
|
name = "vertical"
|
||||||
|
|
||||||
def arrange(self, parent: Widget, size: Size) -> ArrangeResult:
|
def arrange(
|
||||||
|
self, parent: Widget, children: list[Widget], size: Size
|
||||||
|
) -> ArrangeResult:
|
||||||
|
|
||||||
placements: list[WidgetPlacement] = []
|
placements: list[WidgetPlacement] = []
|
||||||
add_placement = placements.append
|
add_placement = placements.append
|
||||||
|
|
||||||
parent_size = parent.outer_size
|
parent_size = parent.outer_size
|
||||||
|
|
||||||
children = list(parent.children)
|
|
||||||
styles = [child.styles for child in children if child.styles.height is not None]
|
styles = [child.styles for child in children if child.styles.height is not None]
|
||||||
total_fraction = sum(
|
total_fraction = sum(
|
||||||
[int(style.height.value) for style in styles if style.height.is_fraction]
|
[int(style.height.value) for style in styles if style.height.is_fraction]
|
||||||
@@ -31,7 +32,7 @@ class VerticalLayout(Layout):
|
|||||||
|
|
||||||
box_models = [
|
box_models = [
|
||||||
widget.get_box_model(size, parent_size, fraction_unit)
|
widget.get_box_model(size, parent_size, fraction_unit)
|
||||||
for widget in parent.children
|
for widget in children
|
||||||
]
|
]
|
||||||
|
|
||||||
margins = [
|
margins = [
|
||||||
@@ -43,8 +44,7 @@ class VerticalLayout(Layout):
|
|||||||
|
|
||||||
y = Fraction(box_models[0].margin.top if box_models else 0)
|
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(children, box_models, margins):
|
||||||
for widget, box_model, margin in zip(displayed_children, box_models, margins):
|
|
||||||
content_width, content_height, box_margin = box_model
|
content_width, content_height, box_margin = box_model
|
||||||
offset_x = (
|
offset_x = (
|
||||||
widget.styles.align_width(
|
widget.styles.align_width(
|
||||||
@@ -60,4 +60,4 @@ class VerticalLayout(Layout):
|
|||||||
total_region = Region(0, 0, size.width, int(y))
|
total_region = Region(0, 0, size.width, int(y))
|
||||||
add_placement(WidgetPlacement(total_region, None, 0))
|
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,
|
Collection,
|
||||||
Iterable,
|
Iterable,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
|
Tuple,
|
||||||
)
|
)
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
@@ -24,7 +25,7 @@ from rich.text import Text
|
|||||||
|
|
||||||
from . import errors, events, messages
|
from . import errors, events, messages
|
||||||
from ._animator import BoundAnimator
|
from ._animator import BoundAnimator
|
||||||
from ._border import Border
|
from ._arrange import arrange
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from ._layout import ArrangeResult, Layout
|
from ._layout import ArrangeResult, Layout
|
||||||
from ._segment_tools import line_crop
|
from ._segment_tools import line_crop
|
||||||
@@ -36,8 +37,6 @@ from .geometry import Offset, Region, Size, Spacing, clamp
|
|||||||
from .layouts.vertical import VerticalLayout
|
from .layouts.vertical import VerticalLayout
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .reactive import Reactive, watch
|
from .reactive import Reactive, watch
|
||||||
from .renderables.opacity import Opacity
|
|
||||||
from .renderables.tint import Tint
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .app import App, ComposeResult
|
from .app import App, ComposeResult
|
||||||
@@ -116,7 +115,7 @@ class Widget(DOMNode):
|
|||||||
self._content_width_cache: tuple[object, int] = (None, 0)
|
self._content_width_cache: tuple[object, int] = (None, 0)
|
||||||
self._content_height_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._arrangement_cache_key: tuple[int, Size] = (-1, Size())
|
||||||
|
|
||||||
self._styles_cache = StylesCache()
|
self._styles_cache = StylesCache()
|
||||||
@@ -146,14 +145,17 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
ArrangeResult: Widget locations.
|
ArrangeResult: Widget locations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
arrange_cache_key = (self.children._updates, size)
|
arrange_cache_key = (self.children._updates, size)
|
||||||
if (
|
if (
|
||||||
self._arrangement is not None
|
self._arrangement is not None
|
||||||
and arrange_cache_key == self._arrangement_cache_key
|
and arrange_cache_key == self._arrangement_cache_key
|
||||||
):
|
):
|
||||||
return self._arrangement
|
return self._arrangement
|
||||||
self._arrangement = self.layout.arrange(self, size)
|
|
||||||
self._arrangement_cache_key = (self.children._updates, size)
|
self._arrangement_cache_key = (self.children._updates, size)
|
||||||
|
|
||||||
|
self._arrangement = arrange(self, size, self.screen.size)
|
||||||
|
|
||||||
return self._arrangement
|
return self._arrangement
|
||||||
|
|
||||||
def _clear_arrangement_cache(self) -> None:
|
def _clear_arrangement_cache(self) -> None:
|
||||||
@@ -542,6 +544,25 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
return self.is_container
|
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:
|
def _set_dirty(self, *regions: Region) -> None:
|
||||||
"""Set the Widget as 'dirty' (requiring re-paint).
|
"""Set the Widget as 'dirty' (requiring re-paint).
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ from ..widget import Widget
|
|||||||
|
|
||||||
|
|
||||||
class Static(Widget):
|
class Static(Widget):
|
||||||
|
CSS = """
|
||||||
|
Static {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
renderable: RenderableType = "",
|
renderable: RenderableType = "",
|
||||||
|
|||||||
Reference in New Issue
Block a user