mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #529 from Textualize/css-add-scrollbar-size
[css] add `scrollbar-size` properties
This commit is contained in:
@@ -74,7 +74,7 @@ App > Screen {
|
||||
|
||||
|
||||
Tweet {
|
||||
height: auto;
|
||||
height: 12;
|
||||
width: 80;
|
||||
|
||||
margin: 1 3;
|
||||
@@ -90,6 +90,15 @@ Tweet {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
Tweet.scrollbar-size-custom {
|
||||
scrollbar-size-vertical: 2;
|
||||
}
|
||||
|
||||
|
||||
Tweet.scroll-horizontal {
|
||||
scrollbar-size-horizontal: 2;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
width: 80;
|
||||
overflow-y: scroll;
|
||||
@@ -114,12 +123,15 @@ TweetHeader {
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
width: 130%;
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
|
||||
}
|
||||
|
||||
Tweet.scroll-horizontal TweetBody {
|
||||
width: 350;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -107,22 +107,15 @@ class BasicApp(App, css_path="basic.css"):
|
||||
),
|
||||
),
|
||||
content=Widget(
|
||||
Tweet(
|
||||
TweetBody(),
|
||||
# Widget(
|
||||
# Widget(classes={"button"}),
|
||||
# Widget(classes={"button"}),
|
||||
# classes={"horizontal"},
|
||||
# ),
|
||||
),
|
||||
Tweet(TweetBody()),
|
||||
Widget(
|
||||
Static(Syntax(CODE, "python"), classes="code"),
|
||||
classes="scrollable",
|
||||
),
|
||||
Error(),
|
||||
Tweet(TweetBody()),
|
||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||
Warning(),
|
||||
Tweet(TweetBody()),
|
||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||
Success(),
|
||||
),
|
||||
footer=Widget(),
|
||||
|
||||
@@ -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:
|
||||
"""Help text to show when the user supplies an invalid value for a `align`.
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ from ._help_text import (
|
||||
offset_single_axis_help_text,
|
||||
style_flags_property_help_text,
|
||||
property_invalid_value_help_text,
|
||||
scrollbar_size_property_help_text,
|
||||
scrollbar_size_single_axis_help_text,
|
||||
)
|
||||
from .constants import (
|
||||
VALID_ALIGN_HORIZONTAL,
|
||||
@@ -784,6 +786,59 @@ class StylesBuilder:
|
||||
else:
|
||||
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:
|
||||
"""
|
||||
Returns a valid CSS property "Python" name, or None if no close matches could be found.
|
||||
|
||||
@@ -129,6 +129,9 @@ class RulesMap(TypedDict, total=False):
|
||||
|
||||
scrollbar_gutter: ScrollbarGutter
|
||||
|
||||
scrollbar_size_vertical: int
|
||||
scrollbar_size_horizontal: int
|
||||
|
||||
align_horizontal: AlignHorizontal
|
||||
align_vertical: AlignVertical
|
||||
|
||||
@@ -228,6 +231,13 @@ class StylesBase(ABC):
|
||||
|
||||
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_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
|
||||
|
||||
@@ -664,6 +674,20 @@ class Styles(StylesBase):
|
||||
append_declaration("overflow-y", self.overflow_y)
|
||||
if has_rule("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"):
|
||||
append_declaration("box-sizing", self.box_sizing)
|
||||
|
||||
@@ -8,17 +8,13 @@ from typing import cast, Iterable
|
||||
|
||||
import rich.repr
|
||||
from rich.console import RenderableType, RenderResult, Console, ConsoleOptions
|
||||
from rich.highlighter import ReprHighlighter
|
||||
from rich.markup import render
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.rule import Rule
|
||||
from rich.style import Style
|
||||
from rich.syntax import Syntax
|
||||
from rich.text import Text
|
||||
|
||||
from .._loop import loop_last
|
||||
from .. import log
|
||||
from .errors import StylesheetError
|
||||
from .match import _check_selectors
|
||||
from .model import RuleSet
|
||||
|
||||
@@ -188,8 +188,11 @@ class ScrollBarRender:
|
||||
|
||||
@rich.repr.auto
|
||||
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.thickness = thickness
|
||||
self.grabbed_position: float = 0
|
||||
super().__init__(name=name)
|
||||
|
||||
@@ -204,6 +207,8 @@ class ScrollBar(Widget):
|
||||
yield "window_virtual_size", self.window_virtual_size
|
||||
yield "window_size", self.window_size
|
||||
yield "position", self.position
|
||||
if self.thickness > 1:
|
||||
yield "thickness", self.thickness
|
||||
|
||||
def render(self, style: Style) -> RenderableType:
|
||||
styles = self.parent.styles
|
||||
@@ -223,6 +228,7 @@ class ScrollBar(Widget):
|
||||
virtual_size=self.window_virtual_size,
|
||||
window_size=self.window_size,
|
||||
position=self.position,
|
||||
thickness=self.thickness,
|
||||
vertical=self.vertical,
|
||||
style=scrollbar_style,
|
||||
)
|
||||
@@ -283,8 +289,12 @@ if __name__ == "__main__":
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
bar = ScrollBarRender()
|
||||
|
||||
console.print(
|
||||
ScrollBarRender(position=15.3, window_size=100, thickness=5, vertical=True)
|
||||
)
|
||||
thickness = 2
|
||||
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))
|
||||
|
||||
@@ -184,6 +184,7 @@ class Widget(DOMNode):
|
||||
int: The optimal width of the content.
|
||||
"""
|
||||
if self.is_container:
|
||||
assert self.layout is not None
|
||||
return (
|
||||
self.layout.get_content_width(self, container, viewport)
|
||||
+ self.scrollbar_width
|
||||
@@ -288,8 +289,9 @@ class Widget(DOMNode):
|
||||
|
||||
if self._vertical_scrollbar is not None:
|
||||
return self._vertical_scrollbar
|
||||
vertical_scrollbar_thickness = self._get_scrollbar_thickness_vertical()
|
||||
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)
|
||||
return scroll_bar
|
||||
@@ -305,8 +307,9 @@ class Widget(DOMNode):
|
||||
|
||||
if self._horizontal_scrollbar is not None:
|
||||
return self._horizontal_scrollbar
|
||||
horizontal_scrollbar_thickness = self._get_scrollbar_thickness_horizontal()
|
||||
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)
|
||||
@@ -360,17 +363,20 @@ class Widget(DOMNode):
|
||||
@property
|
||||
def scrollbar_dimensions(self) -> tuple[int, int]:
|
||||
"""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
|
||||
def scrollbar_width(self) -> int:
|
||||
"""Get the width used by the *vertical* scrollbar."""
|
||||
return int(self.show_vertical_scrollbar)
|
||||
return self._get_scrollbar_thickness_vertical()
|
||||
|
||||
@property
|
||||
def scrollbar_height(self) -> int:
|
||||
"""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:
|
||||
"""Set the Widget as 'dirty' (requiring re-render)."""
|
||||
@@ -574,15 +580,27 @@ class Widget(DOMNode):
|
||||
Region: The widget region minus scrollbars.
|
||||
"""
|
||||
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":
|
||||
# Let's _always_ reserve some space, whether the scrollbar is actually displayed or not:
|
||||
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:
|
||||
(region, _, _, _) = region.split(-1, -1)
|
||||
(region, _, _, _) = region.split(
|
||||
-vertical_scrollbar_thickness, -horizontal_scrollbar_thickness
|
||||
)
|
||||
elif show_vertical_scrollbar:
|
||||
region, _ = region.split_vertical(-1)
|
||||
region, _ = region.split_vertical(-vertical_scrollbar_thickness)
|
||||
elif show_horizontal_scrollbar:
|
||||
region, _ = region.split_horizontal(-1)
|
||||
region, _ = region.split_horizontal(-horizontal_scrollbar_thickness)
|
||||
return region
|
||||
|
||||
def _arrange_scrollbars(self, size: Size) -> Iterable[tuple[Widget, Region]]:
|
||||
@@ -600,26 +618,54 @@ class Widget(DOMNode):
|
||||
region = size.region
|
||||
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:
|
||||
(
|
||||
region,
|
||||
_,
|
||||
vertical_scrollbar_region,
|
||||
horizontal_scrollbar_region,
|
||||
_,
|
||||
) = region.split(-1, -1)
|
||||
) = region.split(
|
||||
-vertical_scrollbar_thickness, -horizontal_scrollbar_thickness
|
||||
)
|
||||
if vertical_scrollbar_region:
|
||||
yield self.vertical_scrollbar, vertical_scrollbar_region
|
||||
if horizontal_scrollbar_region:
|
||||
yield self.horizontal_scrollbar, horizontal_scrollbar_region
|
||||
elif show_vertical_scrollbar:
|
||||
region, scrollbar_region = region.split_vertical(-1)
|
||||
_, scrollbar_region = region.split_vertical(-vertical_scrollbar_thickness)
|
||||
if scrollbar_region:
|
||||
yield self.vertical_scrollbar, scrollbar_region
|
||||
elif show_horizontal_scrollbar:
|
||||
region, scrollbar_region = region.split_horizontal(-1)
|
||||
_, scrollbar_region = region.split_horizontal(
|
||||
-horizontal_scrollbar_thickness
|
||||
)
|
||||
if 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]:
|
||||
"""Pseudo classes for a widget"""
|
||||
if self.mouse_over:
|
||||
|
||||
@@ -198,43 +198,52 @@ def test_widget_style_size_fails_if_data_type_is_not_supported(size_dimension_in
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@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`:
|
||||
# short text: full width, no scrollbar
|
||||
["auto", "auto", "short_text", 80, False],
|
||||
["auto", "auto", 1, "short_text", 80, False],
|
||||
# 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
|
||||
["auto", "stable", "short_text", 79, False],
|
||||
["auto", "stable", 1, "short_text", 79, False],
|
||||
# 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`:
|
||||
# short text: reduced width, scrollbar
|
||||
["scroll", "auto", "short_text", 79, True],
|
||||
["scroll", "auto", 1, "short_text", 79, True],
|
||||
# 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
|
||||
["scroll", "stable", "short_text", 79, True],
|
||||
["scroll", "stable", 1, "short_text", 79, True],
|
||||
# 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`:
|
||||
# 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
|
||||
["hidden", "auto", "long_text", 80, False],
|
||||
["hidden", "auto", 1, "long_text", 80, False],
|
||||
# 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
|
||||
["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(
|
||||
overflow_y: str,
|
||||
scrollbar_gutter: str,
|
||||
scrollbar_size: int,
|
||||
text_length: Literal["short_text", "long_text"],
|
||||
expected_text_widget_width: int,
|
||||
expects_vertical_scrollbar: bool,
|
||||
@@ -254,6 +263,8 @@ async def test_scrollbar_gutter(
|
||||
container.styles.height = 3
|
||||
container.styles.overflow_y = overflow_y
|
||||
container.styles.scrollbar_gutter = scrollbar_gutter
|
||||
if scrollbar_size > 1:
|
||||
container.styles.scrollbar_size_vertical = scrollbar_size
|
||||
|
||||
text_widget = TextWidget()
|
||||
text_widget.styles.height = "auto"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, List
|
||||
from typing import cast, List, Sequence
|
||||
|
||||
import pytest
|
||||
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.parametrize(
|
||||
(
|
||||
"screen_size",
|
||||
"placeholders_count",
|
||||
"root_container_style",
|
||||
"placeholders_style",
|
||||
@@ -35,7 +34,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
|
||||
(
|
||||
*[
|
||||
[
|
||||
SCREEN_SIZE,
|
||||
1,
|
||||
f"border: {invisible_border_edge};", # #root has no visible border
|
||||
"", # 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")
|
||||
],
|
||||
[
|
||||
SCREEN_SIZE,
|
||||
1,
|
||||
"border: solid white;", # #root has a visible border
|
||||
"", # no specific placeholder style
|
||||
@@ -61,7 +58,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
|
||||
1,
|
||||
],
|
||||
[
|
||||
SCREEN_SIZE,
|
||||
4,
|
||||
"border: solid white;", # #root has a visible border
|
||||
"", # no specific placeholder style
|
||||
@@ -73,7 +69,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
|
||||
1,
|
||||
],
|
||||
[
|
||||
SCREEN_SIZE,
|
||||
1,
|
||||
"border: solid white;", # #root has a visible border
|
||||
"align: center top;", # placeholders are centered horizontally
|
||||
@@ -85,7 +80,6 @@ PLACEHOLDERS_DEFAULT_H = 3 # the default height for our Placeholder widgets
|
||||
1,
|
||||
],
|
||||
[
|
||||
SCREEN_SIZE,
|
||||
4,
|
||||
"border: solid white;", # #root has a visible border
|
||||
"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(
|
||||
screen_size: Size,
|
||||
placeholders_count: int,
|
||||
root_container_style: str,
|
||||
placeholders_style: str,
|
||||
@@ -136,9 +129,9 @@ async def test_composition_of_vertical_container_with_children(
|
||||
|
||||
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():
|
||||
# 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_is_a_visible_one = top_left_edge_char != " "
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user