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 {
|
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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user