Repository: Textualize/textual

Issue:
Set scrollbar size in CSS
The scrollbars can be a little hard to click when cells are quite small, particularly a vertical scrollbar.

We should add CSS rules to set the size of scrollbars.

```
scrollbar-size: 2 1;
scrollbar-size-vertical: 2;
scorllbar-size-horizontal: 1;
```

We could make this responsive as the size of the screen changes. For now we just need the rules.

Discussion:
DrBenton: @willmcgugan ready for a last (:crossed_fingers:  ^_^) review: [bd232ac](https://github.com/Textualize/textual/pull/529/commits/bd232ac60a19220a27562ddcad07b4ed771f319f)
willmcgugan: @DrBenton This looks great, but I have probably broken things by adding some scrollbar related code. See https://github.com/Textualize/textual/blob/css/src/textual/widget.py#L347

Can you merge CSS and check that everything renders as expected?
DrBenton: > @DrBenton This looks great, but I have probably broken things by adding some scrollbar related code. See https://github.com/Textualize/textual/blob/css/src/textual/widget.py#L347
> 
> Can you merge CSS and check that everything renders as expected?

It seems to be working! I only had to reverse a few recent testing updates in `basic.css`, so the "basic" sandbox script still looks the same as before. And I also added some custom classes for individual instances of Twet and TweetBody so we can see the various scrollbar types at work. Overall, it seems to be working :slightly_smiling_face: 

We can see on this video:
- first "tweet" has the same default vertical scrollbar than before
- second one has a `scrollbar-size-vertical: 2;`
- third one has a horizontal scrolling, as well as `scrollbar-size-horizontal: 2;`


https://user-images.githubusercontent.com/722388/170017568-854471d2-6b66-4552-bcd5-bf313b022476.mp4



Patch:
diff --git a/sandbox/basic.css b/sandbox/basic.css
index cd32cce1b..4ef50c632 100644
--- a/sandbox/basic.css
+++ b/sandbox/basic.css
@@ -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 {
diff --git a/sandbox/basic.py b/sandbox/basic.py
index a52c05aa5..b444977a6 100644
--- a/sandbox/basic.py
+++ b/sandbox/basic.py
@@ -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(),
diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py
index b3ed0f3ec..fa571f8e8 100644
--- a/src/textual/css/_help_text.py
+++ b/src/textual/css/_help_text.py
@@ -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`.
 
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
index 9f8b959b9..c9d4e40cb 100644
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -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.
diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py
index 44cdae7b5..260bf27d2 100644
--- a/src/textual/css/styles.py
+++ b/src/textual/css/styles.py
@@ -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)
diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py
index ce49fa3f6..cd4d86db8 100644
--- a/src/textual/css/stylesheet.py
+++ b/src/textual/css/stylesheet.py
@@ -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
diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py
index bcf3989aa..2cb5bcd32 100644
--- a/src/textual/scrollbar.py
+++ b/src/textual/scrollbar.py
@@ -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))
diff --git a/src/textual/widget.py b/src/textual/widget.py
index d3bdb491c..bdd5c36f3 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -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:


Test patch:
diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py
index 5d406bcf3..edfe35866 100644
--- a/tests/css/test_styles.py
+++ b/tests/css/test_styles.py
@@ -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"
diff --git a/tests/test_integration_layout.py b/tests/test_integration_layout.py
index 7fde168fa..5ab0facdf 100644
--- a/tests/test_integration_layout.py
+++ b/tests/test_integration_layout.py
@@ -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