Merge pull request #529 from Textualize/css-add-scrollbar-size

[css] add `scrollbar-size` properties
This commit is contained in:
Will McGugan
2022-05-25 17:26:04 +01:00
committed by GitHub
10 changed files with 340 additions and 57 deletions

View File

@@ -74,7 +74,7 @@ App > Screen {
Tweet { Tweet {
height: auto; height: 12;
width: 80; width: 80;
margin: 1 3; margin: 1 3;
@@ -90,6 +90,15 @@ Tweet {
box-sizing: border-box; box-sizing: border-box;
} }
Tweet.scrollbar-size-custom {
scrollbar-size-vertical: 2;
}
Tweet.scroll-horizontal {
scrollbar-size-horizontal: 2;
}
.scrollable { .scrollable {
width: 80; width: 80;
overflow-y: scroll; overflow-y: scroll;
@@ -114,12 +123,15 @@ TweetHeader {
} }
TweetBody { TweetBody {
width: 130%; width: 100%;
background: $panel; background: $panel;
color: $text-panel; color: $text-panel;
height: auto; height: auto;
padding: 0 1 0 0; padding: 0 1 0 0;
}
Tweet.scroll-horizontal TweetBody {
width: 350;
} }
.button { .button {

View File

@@ -107,22 +107,15 @@ class BasicApp(App, css_path="basic.css"):
), ),
), ),
content=Widget( content=Widget(
Tweet( Tweet(TweetBody()),
TweetBody(),
# Widget(
# Widget(classes={"button"}),
# Widget(classes={"button"}),
# classes={"horizontal"},
# ),
),
Widget( Widget(
Static(Syntax(CODE, "python"), classes="code"), Static(Syntax(CODE, "python"), classes="code"),
classes="scrollable", classes="scrollable",
), ),
Error(), Error(),
Tweet(TweetBody()), Tweet(TweetBody(), classes="scrollbar-size-custom"),
Warning(), Warning(),
Tweet(TweetBody()), Tweet(TweetBody(), classes="scroll-horizontal"),
Success(), Success(),
), ),
footer=Widget(), footer=Widget(),

View File

@@ -614,6 +614,67 @@ def offset_property_help_text(context: StylingContext) -> HelpText:
) )
def scrollbar_size_property_help_text(context: StylingContext) -> HelpText:
"""Help text to show when the user supplies an invalid value for the scrollbar-size property.
Args:
context (StylingContext | None): The context the property is being used in.
Returns:
HelpText: Renderable for displaying the help text for this property
"""
return HelpText(
summary="Invalid value for [i]scrollbar-size[/] property",
bullets=[
*ContextSpecificBullets(
inline=[
Bullet(
markup="The [i]scrollbar_size[/] property expects a tuple of 2 values [i](<horizontal>, <vertical>)[/]",
examples=[
Example("widget.styles.scrollbar_size = (2, 1)"),
],
),
],
css=[
Bullet(
markup="The [i]scrollbar-size[/] property expects a value of the form [i]<horizontal> <vertical>[/]",
examples=[
Example(
"scrollbar-size: 2 3; [dim]# Horizontal size of 2, vertical size of 3"
),
],
),
],
).get_by_context(context),
Bullet(
"<horizontal> and <vertical> must be positive integers, greater than zero"
),
],
)
def scrollbar_size_single_axis_help_text(property_name: str) -> HelpText:
"""Help text to show when the user supplies an invalid value for a scrollbar-size-* property.
Args:
property_name (str): The name of the property
Returns:
HelpText: Renderable for displaying the help text for this property
"""
return HelpText(
summary=f"Invalid value for [i]{property_name}[/]",
bullets=[
Bullet(
markup=f"The [i]{property_name}[/] property can only be set to a positive integer, greather than zero",
examples=[
Example(f"{property_name}: 2;"),
],
),
],
)
def align_help_text() -> HelpText: def align_help_text() -> HelpText:
"""Help text to show when the user supplies an invalid value for a `align`. """Help text to show when the user supplies an invalid value for a `align`.

View File

@@ -23,6 +23,8 @@ from ._help_text import (
offset_single_axis_help_text, offset_single_axis_help_text,
style_flags_property_help_text, style_flags_property_help_text,
property_invalid_value_help_text, property_invalid_value_help_text,
scrollbar_size_property_help_text,
scrollbar_size_single_axis_help_text,
) )
from .constants import ( from .constants import (
VALID_ALIGN_HORIZONTAL, VALID_ALIGN_HORIZONTAL,
@@ -784,6 +786,59 @@ class StylesBuilder:
else: else:
self.styles._rules[name.replace("-", "_")] = value self.styles._rules[name.replace("-", "_")] = value
def process_scrollbar_size(self, name: str, tokens: list[Token]) -> None:
def scrollbar_size_error(name: str, token: Token) -> None:
self.error(name, token, scrollbar_size_property_help_text(context="css"))
if not tokens:
return
if len(tokens) != 2:
scrollbar_size_error(name, tokens[0])
else:
token1, token2 = tokens
if token1.name != "number" or not token1.value.isdigit():
scrollbar_size_error(name, token1)
if token2.name != "number" or not token2.value.isdigit():
scrollbar_size_error(name, token2)
horizontal = int(token1.value)
if horizontal == 0:
scrollbar_size_error(name, token1)
vertical = int(token2.value)
if vertical == 0:
scrollbar_size_error(name, token2)
self.styles._rules["scrollbar_size_horizontal"] = horizontal
self.styles._rules["scrollbar_size_vertical"] = vertical
def process_scrollbar_size_vertical(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], scrollbar_size_single_axis_help_text(name))
else:
token = tokens[0]
if token.name != "number" or not token.value.isdigit():
self.error(name, token, scrollbar_size_single_axis_help_text(name))
value = int(token.value)
if value == 0:
self.error(name, token, scrollbar_size_single_axis_help_text(name))
self.styles._rules["scrollbar_size_vertical"] = value
def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], scrollbar_size_single_axis_help_text(name))
else:
token = tokens[0]
if token.name != "number" or not token.value.isdigit():
self.error(name, token, scrollbar_size_single_axis_help_text(name))
value = int(token.value)
if value == 0:
self.error(name, token, scrollbar_size_single_axis_help_text(name))
self.styles._rules["scrollbar_size_horizontal"] = value
def _get_suggested_property_name_for_rule(self, rule_name: str) -> str | None: def _get_suggested_property_name_for_rule(self, rule_name: str) -> str | None:
""" """
Returns a valid CSS property "Python" name, or None if no close matches could be found. Returns a valid CSS property "Python" name, or None if no close matches could be found.

View File

@@ -129,6 +129,9 @@ class RulesMap(TypedDict, total=False):
scrollbar_gutter: ScrollbarGutter scrollbar_gutter: ScrollbarGutter
scrollbar_size_vertical: int
scrollbar_size_horizontal: int
align_horizontal: AlignHorizontal align_horizontal: AlignHorizontal
align_vertical: AlignVertical align_vertical: AlignVertical
@@ -228,6 +231,13 @@ class StylesBase(ABC):
scrollbar_gutter = StringEnumProperty(VALID_SCROLLBAR_GUTTER, "auto") scrollbar_gutter = StringEnumProperty(VALID_SCROLLBAR_GUTTER, "auto")
scrollbar_size_vertical = ScalarProperty(
units={Unit.CELLS}, percent_unit=Unit.WIDTH, allow_auto=False
)
scrollbar_size_horizontal = ScalarProperty(
units={Unit.CELLS}, percent_unit=Unit.HEIGHT, allow_auto=False
)
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left") align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
@@ -664,6 +674,20 @@ class Styles(StylesBase):
append_declaration("overflow-y", self.overflow_y) append_declaration("overflow-y", self.overflow_y)
if has_rule("scrollbar-gutter"): if has_rule("scrollbar-gutter"):
append_declaration("scrollbar-gutter", self.scrollbar_gutter) append_declaration("scrollbar-gutter", self.scrollbar_gutter)
if has_rule("scrollbar-size"):
append_declaration(
"scrollbar-size",
f"{self.scrollbar_size_horizontal} {self.scrollbar_size_vertical}",
)
else:
if has_rule("scrollbar-size-horizontal"):
append_declaration(
"scrollbar-size-horizontal", str(self.scrollbar_size_horizontal)
)
if has_rule("scrollbar-size-vertical"):
append_declaration(
"scrollbar-size-vertical", str(self.scrollbar_size_vertical)
)
if has_rule("box-sizing"): if has_rule("box-sizing"):
append_declaration("box-sizing", self.box_sizing) append_declaration("box-sizing", self.box_sizing)

View File

@@ -8,17 +8,13 @@ from typing import cast, Iterable
import rich.repr import rich.repr
from rich.console import RenderableType, RenderResult, Console, ConsoleOptions from rich.console import RenderableType, RenderResult, Console, ConsoleOptions
from rich.highlighter import ReprHighlighter
from rich.markup import render from rich.markup import render
from rich.padding import Padding from rich.padding import Padding
from rich.panel import Panel from rich.panel import Panel
from rich.rule import Rule
from rich.style import Style from rich.style import Style
from rich.syntax import Syntax from rich.syntax import Syntax
from rich.text import Text from rich.text import Text
from .._loop import loop_last
from .. import log
from .errors import StylesheetError from .errors import StylesheetError
from .match import _check_selectors from .match import _check_selectors
from .model import RuleSet from .model import RuleSet

View File

@@ -188,8 +188,11 @@ class ScrollBarRender:
@rich.repr.auto @rich.repr.auto
class ScrollBar(Widget): class ScrollBar(Widget):
def __init__(self, vertical: bool = True, name: str | None = None) -> None: def __init__(
self, vertical: bool = True, name: str | None = None, *, thickness: int = 1
) -> None:
self.vertical = vertical self.vertical = vertical
self.thickness = thickness
self.grabbed_position: float = 0 self.grabbed_position: float = 0
super().__init__(name=name) super().__init__(name=name)
@@ -204,6 +207,8 @@ class ScrollBar(Widget):
yield "window_virtual_size", self.window_virtual_size yield "window_virtual_size", self.window_virtual_size
yield "window_size", self.window_size yield "window_size", self.window_size
yield "position", self.position yield "position", self.position
if self.thickness > 1:
yield "thickness", self.thickness
def render(self, style: Style) -> RenderableType: def render(self, style: Style) -> RenderableType:
styles = self.parent.styles styles = self.parent.styles
@@ -223,6 +228,7 @@ class ScrollBar(Widget):
virtual_size=self.window_virtual_size, virtual_size=self.window_virtual_size,
window_size=self.window_size, window_size=self.window_size,
position=self.position, position=self.position,
thickness=self.thickness,
vertical=self.vertical, vertical=self.vertical,
style=scrollbar_style, style=scrollbar_style,
) )
@@ -283,8 +289,12 @@ if __name__ == "__main__":
from rich.console import Console from rich.console import Console
console = Console() console = Console()
bar = ScrollBarRender()
console.print( thickness = 2
ScrollBarRender(position=15.3, window_size=100, thickness=5, vertical=True) console.print(f"Bars thickness: {thickness}")
)
console.print("Vertical bar:")
console.print(ScrollBarRender.render_bar(thickness=thickness))
console.print("Horizontal bar:")
console.print(ScrollBarRender.render_bar(vertical=False, thickness=thickness))

View File

@@ -184,6 +184,7 @@ class Widget(DOMNode):
int: The optimal width of the content. int: The optimal width of the content.
""" """
if self.is_container: if self.is_container:
assert self.layout is not None
return ( return (
self.layout.get_content_width(self, container, viewport) self.layout.get_content_width(self, container, viewport)
+ self.scrollbar_width + self.scrollbar_width
@@ -288,8 +289,9 @@ class Widget(DOMNode):
if self._vertical_scrollbar is not None: if self._vertical_scrollbar is not None:
return self._vertical_scrollbar return self._vertical_scrollbar
vertical_scrollbar_thickness = self._get_scrollbar_thickness_vertical()
self._vertical_scrollbar = scroll_bar = ScrollBar( self._vertical_scrollbar = scroll_bar = ScrollBar(
vertical=True, name="vertical" vertical=True, name="vertical", thickness=vertical_scrollbar_thickness
) )
self.app.start_widget(self, scroll_bar) self.app.start_widget(self, scroll_bar)
return scroll_bar return scroll_bar
@@ -305,8 +307,9 @@ class Widget(DOMNode):
if self._horizontal_scrollbar is not None: if self._horizontal_scrollbar is not None:
return self._horizontal_scrollbar return self._horizontal_scrollbar
horizontal_scrollbar_thickness = self._get_scrollbar_thickness_horizontal()
self._horizontal_scrollbar = scroll_bar = ScrollBar( self._horizontal_scrollbar = scroll_bar = ScrollBar(
vertical=False, name="horizontal" vertical=False, name="horizontal", thickness=horizontal_scrollbar_thickness
) )
self.app.start_widget(self, scroll_bar) self.app.start_widget(self, scroll_bar)
@@ -360,17 +363,20 @@ class Widget(DOMNode):
@property @property
def scrollbar_dimensions(self) -> tuple[int, int]: def scrollbar_dimensions(self) -> tuple[int, int]:
"""Get the size of any scrollbars on the widget""" """Get the size of any scrollbars on the widget"""
return (int(self.show_horizontal_scrollbar), int(self.show_vertical_scrollbar)) return (
self._get_scrollbar_thickness_horizontal(),
self._get_scrollbar_thickness_vertical(),
)
@property @property
def scrollbar_width(self) -> int: def scrollbar_width(self) -> int:
"""Get the width used by the *vertical* scrollbar.""" """Get the width used by the *vertical* scrollbar."""
return int(self.show_vertical_scrollbar) return self._get_scrollbar_thickness_vertical()
@property @property
def scrollbar_height(self) -> int: def scrollbar_height(self) -> int:
"""Get the height used by the *horizontal* scrollbar.""" """Get the height used by the *horizontal* scrollbar."""
return int(self.show_horizontal_scrollbar) return self._get_scrollbar_thickness_horizontal()
def set_dirty(self) -> None: def set_dirty(self) -> None:
"""Set the Widget as 'dirty' (requiring re-render).""" """Set the Widget as 'dirty' (requiring re-render)."""
@@ -574,15 +580,27 @@ class Widget(DOMNode):
Region: The widget region minus scrollbars. Region: The widget region minus scrollbars.
""" """
show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled
horizontal_scrollbar_thickness = self._get_scrollbar_thickness_horizontal()
vertical_scrollbar_thickness = self._get_scrollbar_thickness_vertical()
if self.styles.scrollbar_gutter == "stable": if self.styles.scrollbar_gutter == "stable":
# Let's _always_ reserve some space, whether the scrollbar is actually displayed or not: # Let's _always_ reserve some space, whether the scrollbar is actually displayed or not:
show_vertical_scrollbar = True show_vertical_scrollbar = True
vertical_scrollbar_thickness = (
int(self.styles.scrollbar_size_vertical.value)
if self.styles.scrollbar_size_vertical is not None
else 1
)
if show_horizontal_scrollbar and show_vertical_scrollbar: if show_horizontal_scrollbar and show_vertical_scrollbar:
(region, _, _, _) = region.split(-1, -1) (region, _, _, _) = region.split(
-vertical_scrollbar_thickness, -horizontal_scrollbar_thickness
)
elif show_vertical_scrollbar: elif show_vertical_scrollbar:
region, _ = region.split_vertical(-1) region, _ = region.split_vertical(-vertical_scrollbar_thickness)
elif show_horizontal_scrollbar: elif show_horizontal_scrollbar:
region, _ = region.split_horizontal(-1) region, _ = region.split_horizontal(-horizontal_scrollbar_thickness)
return region return region
def _arrange_scrollbars(self, size: Size) -> Iterable[tuple[Widget, Region]]: def _arrange_scrollbars(self, size: Size) -> Iterable[tuple[Widget, Region]]:
@@ -600,26 +618,54 @@ class Widget(DOMNode):
region = size.region region = size.region
show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled
horizontal_scrollbar_thickness = self._get_scrollbar_thickness_horizontal()
vertical_scrollbar_thickness = self._get_scrollbar_thickness_vertical()
if show_horizontal_scrollbar and show_vertical_scrollbar: if show_horizontal_scrollbar and show_vertical_scrollbar:
( (
region, _,
vertical_scrollbar_region, vertical_scrollbar_region,
horizontal_scrollbar_region, horizontal_scrollbar_region,
_, _,
) = region.split(-1, -1) ) = region.split(
-vertical_scrollbar_thickness, -horizontal_scrollbar_thickness
)
if vertical_scrollbar_region: if vertical_scrollbar_region:
yield self.vertical_scrollbar, vertical_scrollbar_region yield self.vertical_scrollbar, vertical_scrollbar_region
if horizontal_scrollbar_region: if horizontal_scrollbar_region:
yield self.horizontal_scrollbar, horizontal_scrollbar_region yield self.horizontal_scrollbar, horizontal_scrollbar_region
elif show_vertical_scrollbar: elif show_vertical_scrollbar:
region, scrollbar_region = region.split_vertical(-1) _, scrollbar_region = region.split_vertical(-vertical_scrollbar_thickness)
if scrollbar_region: if scrollbar_region:
yield self.vertical_scrollbar, scrollbar_region yield self.vertical_scrollbar, scrollbar_region
elif show_horizontal_scrollbar: elif show_horizontal_scrollbar:
region, scrollbar_region = region.split_horizontal(-1) _, scrollbar_region = region.split_horizontal(
-horizontal_scrollbar_thickness
)
if scrollbar_region: if scrollbar_region:
yield self.horizontal_scrollbar, scrollbar_region yield self.horizontal_scrollbar, scrollbar_region
def _get_scrollbar_thickness_horizontal(self) -> int:
"""Get the thickness of the horizontal scrollbar
Returns:
int: the thickness of the horizontal scrollbar (can be zero if CSS rules prevent horizontal scrolling)
"""
scrollbar_size = 1 if self.show_horizontal_scrollbar else 0
if scrollbar_size and self.styles.scrollbar_size_horizontal is not None:
scrollbar_size = int(self.styles.scrollbar_size_horizontal.value)
return scrollbar_size
def _get_scrollbar_thickness_vertical(self) -> int:
"""Get the thickness of the vertical scrollbar
Returns:
int: the thickness of the vertical scrollbar (can be zero if CSS rules prevent vertical scrolling)
"""
scrollbar_size = 1 if self.show_vertical_scrollbar else 0
if scrollbar_size and self.styles.scrollbar_size_vertical is not None:
scrollbar_size = int(self.styles.scrollbar_size_vertical.value)
return scrollbar_size
def get_pseudo_classes(self) -> Iterable[str]: def get_pseudo_classes(self) -> Iterable[str]:
"""Pseudo classes for a widget""" """Pseudo classes for a widget"""
if self.mouse_over: if self.mouse_over:

View File

@@ -198,43 +198,52 @@ def test_widget_style_size_fails_if_data_type_is_not_supported(size_dimension_in
@pytest.mark.asyncio @pytest.mark.asyncio
@pytest.mark.parametrize( @pytest.mark.parametrize(
"overflow_y,scrollbar_gutter,text_length,expected_text_widget_width,expects_vertical_scrollbar", "overflow_y,scrollbar_gutter,scrollbar_size,text_length,expected_text_widget_width,expects_vertical_scrollbar",
( (
# ------------------------------------------------ # ------------------------------------------------
# ----- Let's start with `overflow-y: auto`: # ----- Let's start with `overflow-y: auto`:
# short text: full width, no scrollbar # short text: full width, no scrollbar
["auto", "auto", "short_text", 80, False], ["auto", "auto", 1, "short_text", 80, False],
# long text: reduced width, scrollbar # long text: reduced width, scrollbar
["auto", "auto", "long_text", 79, True], ["auto", "auto", 1, "long_text", 79, True],
# short text, `scrollbar-gutter: stable`: reduced width, no scrollbar # short text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["auto", "stable", "short_text", 79, False], ["auto", "stable", 1, "short_text", 79, False],
# long text, `scrollbar-gutter: stable`: reduced width, scrollbar # long text, `scrollbar-gutter: stable`: reduced width, scrollbar
["auto", "stable", "long_text", 79, True], ["auto", "stable", 1, "long_text", 79, True],
# ------------------------------------------------ # ------------------------------------------------
# ----- And now let's see the behaviour with `overflow-y: scroll`: # ----- And now let's see the behaviour with `overflow-y: scroll`:
# short text: reduced width, scrollbar # short text: reduced width, scrollbar
["scroll", "auto", "short_text", 79, True], ["scroll", "auto", 1, "short_text", 79, True],
# long text: reduced width, scrollbar # long text: reduced width, scrollbar
["scroll", "auto", "long_text", 79, True], ["scroll", "auto", 1, "long_text", 79, True],
# short text, `scrollbar-gutter: stable`: reduced width, scrollbar # short text, `scrollbar-gutter: stable`: reduced width, scrollbar
["scroll", "stable", "short_text", 79, True], ["scroll", "stable", 1, "short_text", 79, True],
# long text, `scrollbar-gutter: stable`: reduced width, scrollbar # long text, `scrollbar-gutter: stable`: reduced width, scrollbar
["scroll", "stable", "long_text", 79, True], ["scroll", "stable", 1, "long_text", 79, True],
# ------------------------------------------------ # ------------------------------------------------
# ----- Finally, let's check the behaviour with `overflow-y: hidden`: # ----- Finally, let's check the behaviour with `overflow-y: hidden`:
# short text: full width, no scrollbar # short text: full width, no scrollbar
["hidden", "auto", "short_text", 80, False], ["hidden", "auto", 1, "short_text", 80, False],
# long text: full width, no scrollbar # long text: full width, no scrollbar
["hidden", "auto", "long_text", 80, False], ["hidden", "auto", 1, "long_text", 80, False],
# short text, `scrollbar-gutter: stable`: reduced width, no scrollbar # short text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["hidden", "stable", "short_text", 79, False], ["hidden", "stable", 1, "short_text", 79, False],
# long text, `scrollbar-gutter: stable`: reduced width, no scrollbar # long text, `scrollbar-gutter: stable`: reduced width, no scrollbar
["hidden", "stable", "long_text", 79, False], ["hidden", "stable", 1, "long_text", 79, False],
# ------------------------------------------------
# ----- Bonus round with a custom scrollbar size, now that we can set this:
["auto", "auto", 3, "short_text", 80, False],
["auto", "auto", 3, "long_text", 77, True],
["scroll", "auto", 3, "short_text", 77, True],
["scroll", "stable", 3, "short_text", 77, True],
["hidden", "auto", 3, "long_text", 80, False],
["hidden", "stable", 3, "short_text", 77, False],
), ),
) )
async def test_scrollbar_gutter( async def test_scrollbar_gutter(
overflow_y: str, overflow_y: str,
scrollbar_gutter: str, scrollbar_gutter: str,
scrollbar_size: int,
text_length: Literal["short_text", "long_text"], text_length: Literal["short_text", "long_text"],
expected_text_widget_width: int, expected_text_widget_width: int,
expects_vertical_scrollbar: bool, expects_vertical_scrollbar: bool,
@@ -254,6 +263,8 @@ async def test_scrollbar_gutter(
container.styles.height = 3 container.styles.height = 3
container.styles.overflow_y = overflow_y container.styles.overflow_y = overflow_y
container.styles.scrollbar_gutter = scrollbar_gutter container.styles.scrollbar_gutter = scrollbar_gutter
if scrollbar_size > 1:
container.styles.scrollbar_size_vertical = scrollbar_size
text_widget = TextWidget() text_widget = TextWidget()
text_widget.styles.height = "auto" text_widget.styles.height = "auto"

View File

@@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
from typing import cast, List from typing import cast, List, Sequence
import pytest import pytest
from rich.console import RenderableType from rich.console import RenderableType
@@ -24,7 +24,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
@pytest.mark.integration_test # this is a slow test, we may want to skip them in some contexts @pytest.mark.integration_test # this is a slow test, we may want to skip them in some contexts
@pytest.mark.parametrize( @pytest.mark.parametrize(
( (
"screen_size",
"placeholders_count", "placeholders_count",
"root_container_style", "root_container_style",
"placeholders_style", "placeholders_style",
@@ -35,7 +34,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
( (
*[ *[
[ [
SCREEN_SIZE,
1, 1,
f"border: {invisible_border_edge};", # #root has no visible border f"border: {invisible_border_edge};", # #root has no visible border
"", # no specific placeholder style "", # no specific placeholder style
@@ -49,7 +47,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
for invisible_border_edge in ("", "none", "hidden") for invisible_border_edge in ("", "none", "hidden")
], ],
[ [
SCREEN_SIZE,
1, 1,
"border: solid white;", # #root has a visible border "border: solid white;", # #root has a visible border
"", # no specific placeholder style "", # no specific placeholder style
@@ -61,7 +58,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
1, 1,
], ],
[ [
SCREEN_SIZE,
4, 4,
"border: solid white;", # #root has a visible border "border: solid white;", # #root has a visible border
"", # no specific placeholder style "", # no specific placeholder style
@@ -73,7 +69,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
1, 1,
], ],
[ [
SCREEN_SIZE,
1, 1,
"border: solid white;", # #root has a visible border "border: solid white;", # #root has a visible border
"align: center top;", # placeholders are centered horizontally "align: center top;", # placeholders are centered horizontally
@@ -85,7 +80,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
1, 1,
], ],
[ [
SCREEN_SIZE,
4, 4,
"border: solid white;", # #root has a visible border "border: solid white;", # #root has a visible border
"align: center top;", # placeholders are centered horizontally "align: center top;", # placeholders are centered horizontally
@@ -99,7 +93,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
), ),
) )
async def test_composition_of_vertical_container_with_children( async def test_composition_of_vertical_container_with_children(
screen_size: Size,
placeholders_count: int, placeholders_count: int,
root_container_style: str, root_container_style: str,
placeholders_style: str, placeholders_style: str,
@@ -136,9 +129,9 @@ async def test_composition_of_vertical_container_with_children(
yield VerticalContainer(*placeholders, id="root") yield VerticalContainer(*placeholders, id="root")
app = MyTestApp(size=screen_size, test_name="compositor") app = MyTestApp(size=SCREEN_SIZE, test_name="compositor")
expected_screen_size = Size(*screen_size) expected_screen_size = SCREEN_SIZE
async with app.in_running_state(): async with app.in_running_state():
# root widget checks: # root widget checks:
@@ -232,3 +225,85 @@ async def test_border_edge_types_impact_on_widget_size(
top_left_edge_char = app.get_char_at(0, 0) top_left_edge_char = app.get_char_at(0, 0)
top_left_edge_char_is_a_visible_one = top_left_edge_char != " " 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 assert top_left_edge_char_is_a_visible_one == expects_visible_char_at_top_left_edge
@pytest.mark.asyncio
@pytest.mark.parametrize(
"large_widget_size,container_style,expected_large_widget_visible_region_size",
(
# In these tests we're going to insert a "large widget"
# into a container with size (20,20).
# ---------------- let's start!
# no overflow/scrollbar instructions: no scrollbars
[Size(30, 30), "color: red", Size(20, 20)],
# explicit hiding of the overflow: no scrollbars either
[Size(30, 30), "overflow: hidden", Size(20, 20)],
# scrollbar for both directions
[Size(30, 30), "overflow: auto", Size(19, 19)],
# horizontal scrollbar
[Size(30, 30), "overflow-x: auto", Size(20, 19)],
# vertical scrollbar
[Size(30, 30), "overflow-y: auto", Size(19, 20)],
# scrollbar for both directions, custom scrollbar size
[Size(30, 30), ("overflow: auto", "scrollbar-size: 3 5"), Size(15, 17)],
# scrollbar for both directions, custom vertical scrollbar size
[Size(30, 30), ("overflow: auto", "scrollbar-size-vertical: 3"), Size(17, 19)],
# scrollbar for both directions, custom horizontal scrollbar size
[
Size(30, 30),
("overflow: auto", "scrollbar-size-horizontal: 3"),
Size(19, 17),
],
# scrollbar needed only vertically, custom scrollbar size
[
Size(20, 30),
("overflow: auto", "scrollbar-size: 3 3"),
Size(17, 20),
],
# scrollbar needed only horizontally, custom scrollbar size
[
Size(30, 20),
("overflow: auto", "scrollbar-size: 3 3"),
Size(20, 17),
],
),
)
async def test_scrollbar_size_impact_on_the_layout(
large_widget_size: Size,
container_style: str | Sequence[str],
expected_large_widget_visible_region_size: Size,
):
class LargeWidget(Widget):
def on_mount(self):
self.styles.width = large_widget_size[0]
self.styles.height = large_widget_size[1]
class LargeWidgetContainer(Widget):
CSS = """
LargeWidgetContainer {
width: 20;
height: 20;
${container_style};
}
""".replace(
"${container_style}",
container_style
if isinstance(container_style, str)
else ";".join(container_style),
)
large_widget = LargeWidget()
container = LargeWidgetContainer(large_widget)
class MyTestApp(AppTest):
def compose(self) -> ComposeResult:
yield container
app = MyTestApp(size=Size(40, 40), test_name="scrollbar_size_impact_on_the_layout")
await app.boot_and_shutdown()
compositor = app.screen._compositor
widgets_map = compositor.map
large_widget_visible_region_size = widgets_map[large_widget].visible_region.size
assert large_widget_visible_region_size == expected_large_widget_visible_region_size