mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' into buttons
This commit is contained in:
@@ -529,6 +529,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
mount_event = events.Mount(sender=self)
|
||||
await self.dispatch_message(mount_event)
|
||||
|
||||
# TODO: don't override `self.console` here
|
||||
self.console = Console(file=sys.__stdout__)
|
||||
self.title = self._title
|
||||
self.refresh()
|
||||
|
||||
@@ -70,7 +70,7 @@ if TYPE_CHECKING:
|
||||
class RulesMap(TypedDict, total=False):
|
||||
"""A typed dict for CSS rules.
|
||||
|
||||
Any key may be absent, indiciating that rule has not been set.
|
||||
Any key may be absent, indicating that rule has not been set.
|
||||
|
||||
Does not define composite rules, that is a rule that is made of a combination of other rules.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ DURATION = r"\d+\.?\d*(?:ms|s)"
|
||||
NUMBER = r"\-?\d+\.?\d*"
|
||||
COLOR = r"\#[0-9a-fA-F]{8}|\#[0-9a-fA-F]{6}|rgb\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)|rgba\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)"
|
||||
KEY_VALUE = r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+"
|
||||
TOKEN = "[a-zA-Z_-]+"
|
||||
TOKEN = "[a-zA-Z][a-zA-Z0-9_-]*"
|
||||
STRING = r"\".*?\""
|
||||
VARIABLE_REF = r"\$[a-zA-Z0-9_\-]+"
|
||||
|
||||
|
||||
@@ -311,6 +311,9 @@ class DOMNode(MessagePump):
|
||||
add_node(node)
|
||||
return nodes
|
||||
|
||||
def displayed_children(self) -> list[DOMNode]:
|
||||
return [child for child in self.children if child.display]
|
||||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
||||
|
||||
|
||||
@@ -50,10 +50,9 @@ class DockLayout(Layout):
|
||||
|
||||
def get_docks(self, parent: Widget) -> list[Dock]:
|
||||
groups: dict[str, list[Widget]] = defaultdict(list)
|
||||
for child in parent.children:
|
||||
for child in parent.displayed_children:
|
||||
assert isinstance(child, Widget)
|
||||
if child.display:
|
||||
groups[child.styles.dock].append(child)
|
||||
groups[child.styles.dock].append(child)
|
||||
docks: list[Dock] = []
|
||||
append_dock = docks.append
|
||||
for name, edge, z in parent.styles.docks:
|
||||
|
||||
@@ -39,7 +39,9 @@ class HorizontalLayout(Layout):
|
||||
|
||||
x = box_models[0].margin.left if box_models else 0
|
||||
|
||||
for widget, box_model, margin in zip(parent.children, box_models, margins):
|
||||
displayed_children = parent.displayed_children
|
||||
|
||||
for widget, box_model, margin in zip(displayed_children, box_models, margins):
|
||||
content_width, content_height = box_model.size
|
||||
offset_y = widget.styles.align_height(content_height, parent_size.height)
|
||||
region = Region(x, offset_y, content_width, content_height)
|
||||
@@ -53,4 +55,4 @@ class HorizontalLayout(Layout):
|
||||
total_region = Region(0, 0, max_width, max_height)
|
||||
add_placement(WidgetPlacement(total_region, None, 0))
|
||||
|
||||
return placements, set(parent.children)
|
||||
return placements, set(displayed_children)
|
||||
|
||||
@@ -40,10 +40,13 @@ class VerticalLayout(Layout):
|
||||
|
||||
y = box_models[0].margin.top if box_models else 0
|
||||
|
||||
for widget, box_model, margin in zip(parent.children, box_models, margins):
|
||||
displayed_children = parent.displayed_children
|
||||
|
||||
for widget, box_model, margin in zip(displayed_children, box_models, margins):
|
||||
content_width, content_height = box_model.size
|
||||
offset_x = widget.styles.align_width(content_width, parent_size.width)
|
||||
region = Region(offset_x, y, content_width, content_height)
|
||||
# TODO: it seems that `max_height` is not used?
|
||||
max_height = max(max_height, content_height)
|
||||
add_placement(WidgetPlacement(region, widget, 0))
|
||||
y += region.height + margin
|
||||
@@ -54,4 +57,4 @@ class VerticalLayout(Layout):
|
||||
total_region = Region(0, 0, max_width, max_height)
|
||||
add_placement(WidgetPlacement(total_region, None, 0))
|
||||
|
||||
return placements, set(parent.children)
|
||||
return placements, set(displayed_children)
|
||||
|
||||
49
tests/css/test_stylesheet.py
Normal file
49
tests/css/test_stylesheet.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
import pytest
|
||||
|
||||
from textual.color import Color
|
||||
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
||||
from textual.css.tokenizer import TokenizeError
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"css_value,expectation,expected_color",
|
||||
[
|
||||
# Valid values:
|
||||
["red", does_not_raise(), Color(128, 0, 0)],
|
||||
["dark_cyan", does_not_raise(), Color(0, 175, 135)],
|
||||
["medium_turquoise", does_not_raise(), Color(95, 215, 215)],
|
||||
["turquoise4", does_not_raise(), Color(0, 135, 135)],
|
||||
["#ffcc00", does_not_raise(), Color(255, 204, 0)],
|
||||
["#ffcc0033", does_not_raise(), Color(255, 204, 0, 0.2)],
|
||||
["rgb(200,90,30)", does_not_raise(), Color(200, 90, 30)],
|
||||
["rgba(200,90,30,0.3)", does_not_raise(), Color(200, 90, 30, 0.3)],
|
||||
# Some invalid ones:
|
||||
["coffee", pytest.raises(StylesheetParseError), None], # invalid color name
|
||||
["turquoise10", pytest.raises(StylesheetParseError), None],
|
||||
["turquoise 4", pytest.raises(StylesheetParseError), None], # space in it
|
||||
["1", pytest.raises(StylesheetParseError), None], # invalid value
|
||||
["()", pytest.raises(TokenizeError), None], # invalid tokens
|
||||
# TODO: implement hex colors with 3 chars? @link https://devdocs.io/css/color_value
|
||||
["#09f", pytest.raises(TokenizeError), None],
|
||||
# TODO: allow spaces in rgb/rgba expressions?
|
||||
["rgb(200, 90, 30)", pytest.raises(TokenizeError), None],
|
||||
["rgba(200,90,30, 0.4)", pytest.raises(TokenizeError), None],
|
||||
],
|
||||
)
|
||||
def test_color_property_parsing(css_value, expectation, expected_color):
|
||||
stylesheet = Stylesheet()
|
||||
css = """
|
||||
* {
|
||||
background: ${COLOR};
|
||||
}
|
||||
""".replace(
|
||||
"${COLOR}", css_value
|
||||
)
|
||||
|
||||
with expectation:
|
||||
stylesheet.parse(css)
|
||||
|
||||
if expected_color:
|
||||
css_rule = stylesheet.rules[0]
|
||||
assert css_rule.styles.background == expected_color
|
||||
30
tests/layouts/test_common_layout_features.py
Normal file
30
tests/layouts/test_common_layout_features.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
|
||||
from textual.screen import Screen
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"layout,display,expected_in_displayed_children",
|
||||
[
|
||||
("dock", "block", True),
|
||||
("horizontal", "block", True),
|
||||
("vertical", "block", True),
|
||||
("dock", "none", False),
|
||||
("horizontal", "none", False),
|
||||
("vertical", "none", False),
|
||||
],
|
||||
)
|
||||
def test_nodes_take_display_property_into_account_when_they_display_their_children(
|
||||
layout: str, display: str, expected_in_displayed_children: bool
|
||||
):
|
||||
widget = Widget(name="widget that might not be visible 🥷 ")
|
||||
widget.styles.display = display
|
||||
|
||||
screen = Screen()
|
||||
screen.styles.layout = layout
|
||||
screen.add_child(widget)
|
||||
|
||||
displayed_children = screen.displayed_children
|
||||
assert isinstance(displayed_children, list)
|
||||
assert (widget in screen.displayed_children) is expected_in_displayed_children
|
||||
Reference in New Issue
Block a user