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(
|
content=Widget(
|
||||||
Tweet(
|
Tweet(TweetBody(), classes="scrollbar-size-test"),
|
||||||
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",
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ class Compositor:
|
|||||||
container_size,
|
container_size,
|
||||||
container_size,
|
container_size,
|
||||||
)
|
)
|
||||||
|
t = 1
|
||||||
|
|
||||||
# Add the container widget, which will render a background
|
# Add the container widget, which will render a background
|
||||||
map[widget] = MapGeometry(
|
map[widget] = MapGeometry(
|
||||||
|
|||||||
@@ -784,6 +784,56 @@ 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 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:
|
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: Scalar
|
||||||
|
scrollbar_size_horizontal: Scalar
|
||||||
|
|
||||||
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,16 @@ 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", 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"):
|
if has_rule("box-sizing"):
|
||||||
append_declaration("box-sizing", self.box_sizing)
|
append_declaration("box-sizing", self.box_sizing)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -288,8 +288,13 @@ 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
|
||||||
|
thickness = (
|
||||||
|
1
|
||||||
|
if self.styles.scrollbar_size_vertical is None
|
||||||
|
else int(self.styles.scrollbar_size_vertical.value)
|
||||||
|
)
|
||||||
self._vertical_scrollbar = scroll_bar = ScrollBar(
|
self._vertical_scrollbar = scroll_bar = ScrollBar(
|
||||||
vertical=True, name="vertical"
|
vertical=True, name="vertical", thickness=thickness
|
||||||
)
|
)
|
||||||
self.app.start_widget(self, scroll_bar)
|
self.app.start_widget(self, scroll_bar)
|
||||||
return scroll_bar
|
return scroll_bar
|
||||||
@@ -305,8 +310,13 @@ 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
|
||||||
|
thickness = (
|
||||||
|
1
|
||||||
|
if self.styles.scrollbar_size_horizontal is None
|
||||||
|
else int(self.styles.scrollbar_size_horizontal.value)
|
||||||
|
)
|
||||||
self._horizontal_scrollbar = scroll_bar = ScrollBar(
|
self._horizontal_scrollbar = scroll_bar = ScrollBar(
|
||||||
vertical=False, name="horizontal"
|
vertical=False, name="horizontal", thickness=thickness
|
||||||
)
|
)
|
||||||
|
|
||||||
self.app.start_widget(self, scroll_bar)
|
self.app.start_widget(self, scroll_bar)
|
||||||
@@ -320,6 +330,9 @@ class Widget(DOMNode):
|
|||||||
styles = self.styles
|
styles = self.styles
|
||||||
overflow_x = styles.overflow_x
|
overflow_x = styles.overflow_x
|
||||||
overflow_y = styles.overflow_y
|
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
|
width, height = self.container_size
|
||||||
|
|
||||||
show_horizontal = self.show_horizontal_scrollbar
|
show_horizontal = self.show_horizontal_scrollbar
|
||||||
@@ -329,6 +342,8 @@ class Widget(DOMNode):
|
|||||||
show_horizontal = True
|
show_horizontal = True
|
||||||
elif overflow_x == "auto":
|
elif overflow_x == "auto":
|
||||||
show_horizontal = self.virtual_size.width > width
|
show_horizontal = self.virtual_size.width > width
|
||||||
|
if scrollbar_size_horizontal == 0:
|
||||||
|
show_horizontal = False
|
||||||
|
|
||||||
show_vertical = self.show_vertical_scrollbar
|
show_vertical = self.show_vertical_scrollbar
|
||||||
if overflow_y == "hidden":
|
if overflow_y == "hidden":
|
||||||
@@ -337,6 +352,8 @@ class Widget(DOMNode):
|
|||||||
show_vertical = True
|
show_vertical = True
|
||||||
elif overflow_y == "auto":
|
elif overflow_y == "auto":
|
||||||
show_vertical = self.virtual_size.height > height
|
show_vertical = self.virtual_size.height > height
|
||||||
|
if scrollbar_size_vertical == 0:
|
||||||
|
show_vertical = False
|
||||||
|
|
||||||
self.show_horizontal_scrollbar = show_horizontal
|
self.show_horizontal_scrollbar = show_horizontal
|
||||||
self.show_vertical_scrollbar = show_vertical
|
self.show_vertical_scrollbar = show_vertical
|
||||||
@@ -577,12 +594,19 @@ class Widget(DOMNode):
|
|||||||
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,
|
||||||
|
horizontal_scrollbar_thickness,
|
||||||
|
) = self._get_scrollbar_thicknesses()
|
||||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
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:
|
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 +624,49 @@ 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
|
||||||
|
|
||||||
|
(
|
||||||
|
vertical_scrollbar_thickness,
|
||||||
|
horizontal_scrollbar_thickness,
|
||||||
|
) = self._get_scrollbar_thicknesses()
|
||||||
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
if show_horizontal_scrollbar and show_vertical_scrollbar:
|
||||||
(
|
(
|
||||||
region,
|
region,
|
||||||
vertical_scrollbar_region,
|
vertical_scrollbar_region,
|
||||||
horizontal_scrollbar_region,
|
horizontal_scrollbar_region,
|
||||||
_,
|
_,
|
||||||
) = region.split(-1, -1)
|
) = region.split(
|
||||||
|
-horizontal_scrollbar_thickness, -vertical_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)
|
region, 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)
|
region, 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_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]:
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user