mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
[css] Add "scrollbar-size" CSS properties - first step
This commit is contained in:
@@ -107,14 +107,7 @@ class BasicApp(App, css_path="basic.css"):
|
||||
),
|
||||
),
|
||||
content=Widget(
|
||||
Tweet(
|
||||
TweetBody(),
|
||||
# Widget(
|
||||
# Widget(classes={"button"}),
|
||||
# Widget(classes={"button"}),
|
||||
# classes={"horizontal"},
|
||||
# ),
|
||||
),
|
||||
Tweet(TweetBody(), classes="scrollbar-size-test"),
|
||||
Widget(
|
||||
Static(Syntax(CODE, "python"), classes="code"),
|
||||
classes="scrollable",
|
||||
|
||||
@@ -356,6 +356,7 @@ class Compositor:
|
||||
container_size,
|
||||
container_size,
|
||||
)
|
||||
t = 1
|
||||
|
||||
# Add the container widget, which will render a background
|
||||
map[widget] = MapGeometry(
|
||||
|
||||
@@ -784,6 +784,56 @@ class StylesBuilder:
|
||||
else:
|
||||
self.styles._rules[name.replace("-", "_")] = value
|
||||
|
||||
def process_scrollbar_size(self, name: str, tokens: list[Token]) -> None:
|
||||
def offset_error(name: str, token: Token) -> None:
|
||||
# TODO: handle help text
|
||||
self.error(name, token, offset_property_help_text(context="css"))
|
||||
|
||||
if not tokens:
|
||||
return
|
||||
if len(tokens) != 2:
|
||||
offset_error(name, tokens[0])
|
||||
else:
|
||||
token1, token2 = tokens
|
||||
|
||||
if token1.name not in ("scalar", "number"):
|
||||
offset_error(name, token1)
|
||||
if token2.name not in ("scalar", "number"):
|
||||
offset_error(name, token2)
|
||||
|
||||
vertical = Scalar.parse(token1.value, Unit.HEIGHT)
|
||||
horizontal = Scalar.parse(token2.value, Unit.WIDTH)
|
||||
self.styles._rules["scrollbar_size_vertical"] = vertical
|
||||
self.styles._rules["scrollbar_size_horizontal"] = horizontal
|
||||
|
||||
def process_scrollbar_size_vertical(self, name: str, tokens: list[Token]) -> None:
|
||||
if not tokens:
|
||||
return
|
||||
if len(tokens) != 1:
|
||||
# TODO: handle help text
|
||||
self.error(name, tokens[0], offset_single_axis_help_text(name))
|
||||
else:
|
||||
token = tokens[0]
|
||||
if token.name not in ("scalar", "number"):
|
||||
self.error(name, token, offset_single_axis_help_text(name))
|
||||
self.styles._rules["scrollbar_size_vertical"] = Scalar.parse(
|
||||
token.value, Unit.HEIGHT
|
||||
)
|
||||
|
||||
def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> None:
|
||||
if not tokens:
|
||||
return
|
||||
if len(tokens) != 1:
|
||||
# TODO: handle help text
|
||||
self.error(name, tokens[0], offset_single_axis_help_text(name))
|
||||
else:
|
||||
token = tokens[0]
|
||||
if token.name not in ("scalar", "number"):
|
||||
self.error(name, token, offset_single_axis_help_text(name))
|
||||
self.styles._rules["scrollbar_size_horizontal"] = Scalar.parse(
|
||||
token.value, Unit.WIDTH
|
||||
)
|
||||
|
||||
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: Scalar
|
||||
scrollbar_size_horizontal: Scalar
|
||||
|
||||
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,16 @@ 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", self.scrollbar_size)
|
||||
if has_rule("scrollbar-size-vertical"):
|
||||
append_declaration(
|
||||
"scrollbar-size-vertical", str(self.scrollbar_size_vertical)
|
||||
)
|
||||
if has_rule("scrollbar-size-horizontal"):
|
||||
append_declaration(
|
||||
"scrollbar-size-horizontal", str(self.scrollbar_size_horizontal)
|
||||
)
|
||||
|
||||
if has_rule("box-sizing"):
|
||||
append_declaration("box-sizing", self.box_sizing)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -288,8 +288,13 @@ class Widget(DOMNode):
|
||||
|
||||
if self._vertical_scrollbar is not None:
|
||||
return self._vertical_scrollbar
|
||||
thickness = (
|
||||
1
|
||||
if self.styles.scrollbar_size_vertical is None
|
||||
else int(self.styles.scrollbar_size_vertical.value)
|
||||
)
|
||||
self._vertical_scrollbar = scroll_bar = ScrollBar(
|
||||
vertical=True, name="vertical"
|
||||
vertical=True, name="vertical", thickness=thickness
|
||||
)
|
||||
self.app.start_widget(self, scroll_bar)
|
||||
return scroll_bar
|
||||
@@ -305,8 +310,13 @@ class Widget(DOMNode):
|
||||
|
||||
if self._horizontal_scrollbar is not None:
|
||||
return self._horizontal_scrollbar
|
||||
thickness = (
|
||||
1
|
||||
if self.styles.scrollbar_size_horizontal is None
|
||||
else int(self.styles.scrollbar_size_horizontal.value)
|
||||
)
|
||||
self._horizontal_scrollbar = scroll_bar = ScrollBar(
|
||||
vertical=False, name="horizontal"
|
||||
vertical=False, name="horizontal", thickness=thickness
|
||||
)
|
||||
|
||||
self.app.start_widget(self, scroll_bar)
|
||||
@@ -320,6 +330,9 @@ class Widget(DOMNode):
|
||||
styles = self.styles
|
||||
overflow_x = styles.overflow_x
|
||||
overflow_y = styles.overflow_y
|
||||
scrollbar_size_horizontal = styles.scrollbar_size_horizontal
|
||||
scrollbar_size_vertical = styles.scrollbar_size_vertical
|
||||
overflow_y = styles.overflow_y
|
||||
width, height = self.container_size
|
||||
|
||||
show_horizontal = self.show_horizontal_scrollbar
|
||||
@@ -329,6 +342,8 @@ class Widget(DOMNode):
|
||||
show_horizontal = True
|
||||
elif overflow_x == "auto":
|
||||
show_horizontal = self.virtual_size.width > width
|
||||
if scrollbar_size_horizontal == 0:
|
||||
show_horizontal = False
|
||||
|
||||
show_vertical = self.show_vertical_scrollbar
|
||||
if overflow_y == "hidden":
|
||||
@@ -337,6 +352,8 @@ class Widget(DOMNode):
|
||||
show_vertical = True
|
||||
elif overflow_y == "auto":
|
||||
show_vertical = self.virtual_size.height > height
|
||||
if scrollbar_size_vertical == 0:
|
||||
show_vertical = False
|
||||
|
||||
self.show_horizontal_scrollbar = show_horizontal
|
||||
self.show_vertical_scrollbar = show_vertical
|
||||
@@ -577,12 +594,19 @@ class Widget(DOMNode):
|
||||
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,
|
||||
horizontal_scrollbar_thickness,
|
||||
) = self._get_scrollbar_thicknesses()
|
||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
||||
(region, _, _, _) = region.split(-1, -1)
|
||||
(region, _, _, _) = region.split(
|
||||
-horizontal_scrollbar_thickness, -vertical_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 +624,49 @@ class Widget(DOMNode):
|
||||
region = size.region
|
||||
show_vertical_scrollbar, show_horizontal_scrollbar = self.scrollbars_enabled
|
||||
|
||||
(
|
||||
vertical_scrollbar_thickness,
|
||||
horizontal_scrollbar_thickness,
|
||||
) = self._get_scrollbar_thicknesses()
|
||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
||||
(
|
||||
region,
|
||||
vertical_scrollbar_region,
|
||||
horizontal_scrollbar_region,
|
||||
_,
|
||||
) = region.split(-1, -1)
|
||||
) = region.split(
|
||||
-horizontal_scrollbar_thickness, -vertical_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)
|
||||
region, 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)
|
||||
region, scrollbar_region = region.split_horizontal(
|
||||
-horizontal_scrollbar_thickness
|
||||
)
|
||||
if scrollbar_region:
|
||||
yield self.horizontal_scrollbar, scrollbar_region
|
||||
|
||||
def _get_scrollbar_thicknesses(self) -> tuple[int, int]:
|
||||
"""
|
||||
Returns:
|
||||
tuple[int, int]: first integer is the thickness of the vertical scrollbar,
|
||||
2nd integer is the thickness of the horizontal scrollbar
|
||||
"""
|
||||
vertical_scrollbar_size = horizontal_scrollbar_size = 1
|
||||
if self.styles.scrollbar_size_vertical is not None:
|
||||
vertical_scrollbar_size = int(self.styles.scrollbar_size_vertical.value)
|
||||
if self.styles.scrollbar_size_horizontal is not None:
|
||||
horizontal_scrollbar_size = int(self.styles.scrollbar_size_horizontal.value)
|
||||
return vertical_scrollbar_size, horizontal_scrollbar_size
|
||||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
"""Pseudo classes for a widget"""
|
||||
if self.mouse_over:
|
||||
|
||||
Reference in New Issue
Block a user