[css] Add a test for the impact of our border edge types on the layout

This commit is contained in:
Olivier Philippon
2022-05-18 11:40:53 +01:00
parent b135fa784b
commit af2f1580ce
6 changed files with 152 additions and 54 deletions

View File

@@ -17,7 +17,7 @@ class VerticalContainer(Widget):
VerticalContainer Placeholder { VerticalContainer Placeholder {
margin: 1 0; margin: 1 0;
height: 5; height: auto;
align: center top; align: center top;
} }
""" """
@@ -37,26 +37,28 @@ class Introduction(Widget):
return Text("Here are the color edge types we support.", justify="center") return Text("Here are the color edge types we support.", justify="center")
class BorderDemo(Widget):
def __init__(self, name: str):
super().__init__(name=name)
def render(self, style) -> RenderableType:
return Text(self.name, style="black on yellow", justify="center")
class MyTestApp(App): class MyTestApp(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
placeholders = [] border_demo_widgets = []
for border_edge_type in EdgeType.__args__: for border_edge_type in EdgeType.__args__:
border_placeholder = Placeholder( border_demo = BorderDemo(f'"border: {border_edge_type} white"')
id=f"placeholder_{border_edge_type}", border_demo.styles.height = "auto"
title=(border_edge_type or " ").upper(), border_demo.styles.margin = (1, 0)
name=f"border: {border_edge_type} white", border_demo.styles.border = (border_edge_type, "white")
) border_demo_widgets.append(border_demo)
border_placeholder.styles.border = (border_edge_type, "white")
placeholders.append(border_placeholder)
yield VerticalContainer(Introduction(), *placeholders, id="root") yield VerticalContainer(Introduction(), *border_demo_widgets, id="root")
def on_mount(self): def on_mount(self):
self.bind("q", "quit") self.bind("q", "quit")
self.bind("t", "tree")
def action_tree(self):
self.log(self.tree)
app = MyTestApp() app = MyTestApp()

View File

@@ -57,7 +57,7 @@ BORDER_LOCATIONS: dict[
"wide": ((1, 1, 1), (0, 1, 0), (1, 1, 1)), "wide": ((1, 1, 1), (0, 1, 0), (1, 1, 1)),
} }
_INVISIBLE_EDGE_TYPES: tuple[EdgeType, ...] = ("none", "hidden") INVISIBLE_EDGE_TYPES: tuple[EdgeType, ...] = ("", "none", "hidden")
@lru_cache(maxsize=1024) @lru_cache(maxsize=1024)
@@ -170,10 +170,10 @@ class Border:
width (int): Desired width. width (int): Desired width.
""" """
top, right, bottom, left = self._sides top, right, bottom, left = self._sides
has_left = left not in _INVISIBLE_EDGE_TYPES has_left = left not in INVISIBLE_EDGE_TYPES
has_right = right not in _INVISIBLE_EDGE_TYPES has_right = right not in INVISIBLE_EDGE_TYPES
has_top = top not in _INVISIBLE_EDGE_TYPES has_top = top not in INVISIBLE_EDGE_TYPES
has_bottom = bottom not in _INVISIBLE_EDGE_TYPES has_bottom = bottom not in INVISIBLE_EDGE_TYPES
if has_top: if has_top:
lines.pop(0) lines.pop(0)
@@ -199,10 +199,10 @@ class Border:
outer_style = console.get_style(self.outer_style) outer_style = console.get_style(self.outer_style)
top_style, right_style, bottom_style, left_style = self._styles top_style, right_style, bottom_style, left_style = self._styles
has_left = left not in _INVISIBLE_EDGE_TYPES has_left = left not in INVISIBLE_EDGE_TYPES
has_right = right not in _INVISIBLE_EDGE_TYPES has_right = right not in INVISIBLE_EDGE_TYPES
has_top = top not in _INVISIBLE_EDGE_TYPES has_top = top not in INVISIBLE_EDGE_TYPES
has_bottom = bottom not in _INVISIBLE_EDGE_TYPES has_bottom = bottom not in INVISIBLE_EDGE_TYPES
width = options.max_width - has_left - has_right width = options.max_width - has_left - has_right

View File

@@ -27,6 +27,7 @@ from ._help_text import (
string_enum_help_text, string_enum_help_text,
color_property_help_text, color_property_help_text,
) )
from .._border import INVISIBLE_EDGE_TYPES
from ..color import Color, ColorPair, ColorParseError from ..color import Color, ColorPair, ColorParseError
from ._error_tools import friendly_list from ._error_tools import friendly_list
from .constants import NULL_SPACING, VALID_STYLE_FLAGS from .constants import NULL_SPACING, VALID_STYLE_FLAGS
@@ -243,10 +244,10 @@ class Edges(NamedTuple):
""" """
(top, _), (right, _), (bottom, _), (left, _) = self (top, _), (right, _), (bottom, _), (left, _) = self
return Spacing( return Spacing(
1 if top else 0, 1 if top not in INVISIBLE_EDGE_TYPES else 0,
1 if right else 0, 1 if right not in INVISIBLE_EDGE_TYPES else 0,
1 if bottom else 0, 1 if bottom not in INVISIBLE_EDGE_TYPES else 0,
1 if left else 0, 1 if left not in INVISIBLE_EDGE_TYPES else 0,
) )

View File

@@ -42,7 +42,7 @@ from .scalar import Scalar, ScalarOffset, Unit, ScalarError, ScalarParseError
from .styles import DockGroup, Styles from .styles import DockGroup, Styles
from .tokenize import Token from .tokenize import Token
from .transition import Transition from .transition import Transition
from .types import BoxSizing, Edge, Display, Overflow, Visibility from .types import BoxSizing, Edge, Display, Overflow, Visibility, EdgeType
from ..color import Color, ColorParseError from ..color import Color, ColorParseError
from .._duration import _duration_as_seconds from .._duration import _duration_as_seconds
from .._easing import EASING from .._easing import EASING
@@ -418,7 +418,7 @@ class StylesBuilder:
process_padding_left = _process_space_partial process_padding_left = _process_space_partial
def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Color]: def _parse_border(self, name: str, tokens: list[Token]) -> tuple[str, Color]:
border_type = "solid" border_type: EdgeType = "solid"
border_color = Color(0, 255, 0) border_color = Color(0, 255, 0)
def border_value_error(): def border_value_error():

View File

@@ -3,9 +3,12 @@ import asyncio
from typing import cast, List from typing import cast, List
import pytest import pytest
from rich.console import RenderableType
from rich.text import Text
from tests.utilities.test_app import AppTest from tests.utilities.test_app import AppTest
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.css.types import EdgeType
from textual.geometry import Size from textual.geometry import Size
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Placeholder from textual.widgets import Placeholder
@@ -31,30 +34,20 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
"expected_placeholders_offset_x", "expected_placeholders_offset_x",
), ),
( (
[ *[
SCREEN_SIZE, [
1, SCREEN_SIZE,
"border: ;", # #root has no border 1,
"", # no specific placeholder style f"border: {invisible_border_edge};", # #root has no visible border
# #root's virtual size=screen size "", # no specific placeholder style
(SCREEN_W, SCREEN_H), # #root's virtual size=screen size
# placeholders width=same than screen :: height=default height (SCREEN_W, SCREEN_H),
(SCREEN_W, PLACEHOLDERS_DEFAULT_H), # placeholders width=same than screen :: height=default height
# placeholders should be at offset 0 (SCREEN_W, PLACEHOLDERS_DEFAULT_H),
0, # placeholders should be at offset 0
], 0,
[ ]
# "none" borders still allocate a space for the (invisible) border for invisible_border_edge in ("", "none", "hidden")
SCREEN_SIZE,
1,
"border: none;", # #root has an invisible border
"", # no specific placeholder style
# #root's virtual size is smaller because of its borders
(SCREEN_W - 2, SCREEN_H - 2),
# placeholders width=same than screen, minus 2 borders :: height=default height minus 2 borders
(SCREEN_W - 2, PLACEHOLDERS_DEFAULT_H),
# placeholders should be at offset 1 because of #root's border
1,
], ],
[ [
SCREEN_SIZE, SCREEN_SIZE,
@@ -169,3 +162,75 @@ async def test_composition_of_vertical_container_with_children(
assert placeholder.size == expected_placeholders_size assert placeholder.size == expected_placeholders_size
assert placeholder.styles.offset.x.value == 0.0 assert placeholder.styles.offset.x.value == 0.0
assert app.screen.get_offset(placeholder).x == expected_placeholders_offset_x assert app.screen.get_offset(placeholder).x == expected_placeholders_offset_x
@pytest.mark.asyncio
@pytest.mark.integration_test
@pytest.mark.parametrize(
"edge_type,expected_box_inner_size,expected_box_size,expected_top_left_edge_color,expects_visible_char_at_top_left_edge",
(
# These first 3 types of border edge types are synonyms, and display no borders:
["", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
["none", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
["hidden", Size(SCREEN_W, 1), Size(SCREEN_W, 1), "black", False],
# Let's transition to "blank": we still see no visible border, but the size is increased
# as the gutter space is reserved the same way it would be with a border:
["blank", Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", False],
# And now for the "normally visible" border edge types:
# --> we see a visible border, and the size is increased:
*[
[edge_style, Size(SCREEN_W - 2, 1), Size(SCREEN_W, 3), "#ffffff", True]
for edge_style in [
"round",
"solid",
"double",
"dashed",
"heavy",
"inner",
"outer",
"hkey",
"vkey",
"tall",
"wide",
]
],
),
)
async def test_border_edge_types_impact_on_widget_size(
edge_type: EdgeType,
expected_box_inner_size: Size,
expected_box_size: Size,
expected_top_left_edge_color: str,
expects_visible_char_at_top_left_edge: bool,
):
class BorderTarget(Widget):
def render(self, style) -> RenderableType:
return Text("border target", style="black on yellow", justify="center")
border_target = BorderTarget()
border_target.styles.height = "auto"
border_target.styles.border = (edge_type, "white")
class MyTestApp(AppTest):
def compose(self) -> ComposeResult:
yield border_target
app = MyTestApp(size=SCREEN_SIZE, test_name="border_edge_types")
await app.boot_and_shutdown()
box_inner_size = Size(
border_target.content_region.width,
border_target.content_region.height,
)
assert box_inner_size == expected_box_inner_size
assert border_target.size == expected_box_size
top_left_edge_style = app.screen.get_style_at(0, 0)
top_left_edge_color = top_left_edge_style.color.name
assert top_left_edge_color == expected_top_left_edge_color
top_left_edge_char = app.get_char_at(0, 0)
top_left_edge_char_is_a_visible_one = top_left_edge_char != " "
assert top_left_edge_char_is_a_visible_one == expects_visible_char_at_top_left_edge

View File

@@ -8,7 +8,7 @@ from typing import AsyncContextManager, cast
from rich.console import Console from rich.console import Console
from textual import events from textual import events, errors
from textual.app import App, ReturnType, ComposeResult from textual.app import App, ReturnType, ComposeResult
from textual.driver import Driver from textual.driver import Driver
from textual.geometry import Size from textual.geometry import Size
@@ -123,6 +123,36 @@ class AppTest(App):
last_display_start_index = total_capture.rindex(CLEAR_SCREEN_SEQUENCE) last_display_start_index = total_capture.rindex(CLEAR_SCREEN_SEQUENCE)
return total_capture[last_display_start_index:] return total_capture[last_display_start_index:]
def get_char_at(self, x: int, y: int) -> str:
"""Get the character at the given cell or empty string
Args:
x (int): X position within the Layout
y (int): Y position within the Layout
Returns:
str: The character at the cell (x, y) within the Layout
"""
# N.B. Basically a copy-paste-and-slightly-adapt of `Compositor.get_style_at()`
try:
widget, region = self.get_widget_at(x, y)
except errors.NoWidget:
return ""
if widget not in self.screen._compositor.regions:
return ""
x -= region.x
y -= region.y
lines = widget.get_render_lines(y, y + 1)
if not lines:
return ""
end = 0
for segment in lines[0]:
end += segment.cell_length
if x < end:
return segment.text[0]
return ""
@property @property
def console(self) -> ConsoleTest: def console(self) -> ConsoleTest:
return self._console return self._console