diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76b63dc91..e48eb1545 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,10 +21,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Scrolling with cursor keys now moves just one cell https://github.com/Textualize/textual/issues/1897
-
-### Fixed
-
- Fix exceptions in watch methods being hidden on startup https://github.com/Textualize/textual/issues/1886
+- Fixed scrollbar size miscalculation https://github.com/Textualize/textual/pull/1910
+- Fixed slow exit on some terminals https://github.com/Textualize/textual/issues/1920
## [0.12.1] - 2023-02-25
diff --git a/src/textual/_layout.py b/src/textual/_layout.py
index f7ab16312..0cdfe5956 100644
--- a/src/textual/_layout.py
+++ b/src/textual/_layout.py
@@ -25,6 +25,7 @@ class DockArrangeResult:
"""Shared spacing around the widgets."""
_spatial_map: SpatialMap[WidgetPlacement] | None = None
+ """A Spatial map to query widget placements."""
@property
def spatial_map(self) -> SpatialMap[WidgetPlacement]:
@@ -111,14 +112,8 @@ class Layout(ABC):
width = 0
else:
# Use a size of 0, 0 to ignore relative sizes, since those are flexible anyway
- placements = widget._arrange(Size(0, 0)).placements
- width = max(
- [
- placement.region.right + placement.margin.right
- for placement in placements
- ],
- default=0,
- )
+ arrangement = widget._arrange(Size(0, 0))
+ return arrangement.total_region.right + arrangement.spacing.right
return width
def get_content_height(
@@ -139,13 +134,6 @@ class Layout(ABC):
height = 0
else:
# Use a height of zero to ignore relative heights
- placements = widget._arrange(Size(width, 0)).placements
- height = max(
- [
- placement.region.bottom + placement.margin.bottom
- for placement in placements
- ],
- default=0,
- )
-
+ arrangement = widget._arrange(Size(width, 0))
+ height = arrangement.total_region.bottom + arrangement.spacing.bottom
return height
diff --git a/src/textual/app.py b/src/textual/app.py
index fa70671b0..3ca169e85 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -1817,7 +1817,7 @@ class App(Generic[ReturnType], DOMNode):
await child._close_messages()
async def _shutdown(self) -> None:
- self._begin_update() # Prevents any layout / repaint while shutting down
+ self._begin_batch() # Prevents any layout / repaint while shutting down
driver = self._driver
self._running = False
if driver is not None:
diff --git a/src/textual/box_model.py b/src/textual/box_model.py
index d43c9b3ff..85f57d9ca 100644
--- a/src/textual/box_model.py
+++ b/src/textual/box_model.py
@@ -125,6 +125,7 @@ def get_box_model(
content_height = min(content_height, max_height)
content_height = max(Fraction(0), content_height)
+
model = BoxModel(
content_width + gutter.width, content_height + gutter.height, margin
)
diff --git a/src/textual/cli/previews/colors.css b/src/textual/cli/previews/colors.css
index caa950430..38a6ce045 100644
--- a/src/textual/cli/previews/colors.css
+++ b/src/textual/cli/previews/colors.css
@@ -9,7 +9,7 @@ ColorButtons {
}
ColorButtons > Button {
- width: 30;
+ width: 100%;
}
ColorsView {
diff --git a/src/textual/constants.py b/src/textual/constants.py
index 41bbb7ca1..2e57942a7 100644
--- a/src/textual/constants.py
+++ b/src/textual/constants.py
@@ -3,7 +3,6 @@ Constants that we might want to expose via the public API.
"""
-
import os
from typing_extensions import Final
diff --git a/src/textual/demo.css b/src/textual/demo.css
index 9941d8ba5..ec352ea21 100644
--- a/src/textual/demo.css
+++ b/src/textual/demo.css
@@ -102,6 +102,7 @@ Column {
height: auto;
min-height: 100vh;
align: center top;
+ overflow: hidden;
}
diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py
index 448df5323..4697da2c6 100644
--- a/src/textual/message_pump.py
+++ b/src/textual/message_pump.py
@@ -340,6 +340,7 @@ class MessagePump(metaclass=MessagePumpMeta):
"""
# We send the InvokeLater message to ourselves first, to ensure we've cleared
# out anything already pending in our own queue.
+
message = messages.InvokeLater(self, partial(callback, *args, **kwargs))
self.post_message_no_wait(message)
diff --git a/src/textual/scroll_view.py b/src/textual/scroll_view.py
index 1f834e839..dcf6b17cf 100644
--- a/src/textual/scroll_view.py
+++ b/src/textual/scroll_view.py
@@ -82,7 +82,7 @@ class ScrollView(Widget):
Returns:
True if anything changed, or False if nothing changed.
"""
- if self._size != size or container_size != container_size:
+ if self._size != size or self._container_size != container_size:
self.refresh()
if (
self._size != size
@@ -93,7 +93,6 @@ class ScrollView(Widget):
virtual_size = self.virtual_size
self._container_size = size - self.styles.gutter.totals
self._scroll_update(virtual_size)
- self.scroll_to(self.scroll_x, self.scroll_y, animate=False)
return True
else:
return False
diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py
index 3e9acf913..46a65e27b 100644
--- a/src/textual/scrollbar.py
+++ b/src/textual/scrollbar.py
@@ -197,7 +197,7 @@ class ScrollBarRender:
class ScrollBar(Widget):
renderer: ClassVar[Type[ScrollBarRender]] = ScrollBarRender
"""The class used for rendering scrollbars.
- This can be overriden and set to a ScrollBarRender-derived class
+ This can be overridden and set to a ScrollBarRender-derived class
in order to delegate all scrollbar rendering to that class. E.g.:
```
diff --git a/src/textual/widget.py b/src/textual/widget.py
index 3dd5f35d8..ea1a70c3c 100644
--- a/src/textual/widget.py
+++ b/src/textual/widget.py
@@ -274,7 +274,8 @@ class Widget(DOMNode):
self._styles_cache = StylesCache()
self._rich_style_cache: dict[str, tuple[Style, Style]] = {}
- self._stabilized_scrollbar_size: Size | None = None
+ self._stabilize_scrollbar: tuple[Size, str, str] | None = None
+ """Used to prevent scrollbar logic getting stuck in an infinite loop."""
self._lock = Lock()
super().__init__(
@@ -520,6 +521,7 @@ class Widget(DOMNode):
def _clear_arrangement_cache(self) -> None:
"""Clear arrangement cache, forcing a new arrange operation."""
self._arrangement_cache.clear()
+ self._stabilize_scrollbar = None
def _get_virtual_dom(self) -> Iterable[Widget]:
"""Get widgets not part of the DOM.
@@ -855,14 +857,11 @@ class Widget(DOMNode):
"""
if self.is_container:
assert self._layout is not None
- height = (
- self._layout.get_content_height(
- self,
- container,
- viewport,
- width,
- )
- + self.scrollbar_size_horizontal
+ height = self._layout.get_content_height(
+ self,
+ container,
+ viewport,
+ width,
)
else:
cache_key = width
@@ -913,8 +912,7 @@ class Widget(DOMNode):
return max(
0,
self.virtual_size.width
- - self.container_size.width
- + self.scrollbar_size_vertical,
+ - (self.container_size.width - self.scrollbar_size_vertical),
)
@property
@@ -923,8 +921,7 @@ class Widget(DOMNode):
return max(
0,
self.virtual_size.height
- - self.container_size.height
- + self.scrollbar_size_horizontal,
+ - (self.container_size.height - self.scrollbar_size_horizontal),
)
@property
@@ -985,9 +982,18 @@ class Widget(DOMNode):
styles = self.styles
overflow_x = styles.overflow_x
overflow_y = styles.overflow_y
- width, height = self.container_size
- show_horizontal = self.show_horizontal_scrollbar
+ stabilize_scrollbar = (
+ self.container_size,
+ overflow_x,
+ overflow_y,
+ )
+ if self._stabilize_scrollbar == stabilize_scrollbar:
+ return
+
+ width, height = self._container_size
+
+ show_horizontal = False
if overflow_x == "hidden":
show_horizontal = False
elif overflow_x == "scroll":
@@ -995,7 +1001,7 @@ class Widget(DOMNode):
elif overflow_x == "auto":
show_horizontal = self.virtual_size.width > width
- show_vertical = self.show_vertical_scrollbar
+ show_vertical = False
if overflow_y == "hidden":
show_vertical = False
elif overflow_y == "scroll":
@@ -1003,16 +1009,17 @@ class Widget(DOMNode):
elif overflow_y == "auto":
show_vertical = self.virtual_size.height > height
- if (
- overflow_x == "auto"
- and show_vertical
- and not show_horizontal
- and self._stabilized_scrollbar_size != self.container_size
- ):
- show_horizontal = (
- self.virtual_size.width + styles.scrollbar_size_vertical > width
+ # When a single scrollbar is shown, the other dimension changes, so we need to recalculate.
+ if show_vertical and not show_horizontal:
+ show_horizontal = self.virtual_size.width > (
+ width - styles.scrollbar_size_vertical
)
- self._stabilized_scrollbar_size = self.container_size
+ if show_horizontal and not show_vertical:
+ show_vertical = self.virtual_size.height > (
+ height - styles.scrollbar_size_horizontal
+ )
+
+ self._stabilize_scrollbar = stabilize_scrollbar
self.show_horizontal_scrollbar = show_horizontal
self.show_vertical_scrollbar = show_vertical
@@ -1454,6 +1461,7 @@ class Widget(DOMNode):
Returns:
`True` if the scroll position changed, otherwise `False`.
"""
+
maybe_scroll_x = x is not None and (self.allow_horizontal_scroll or force)
maybe_scroll_y = y is not None and (self.allow_vertical_scroll or force)
scrolled_x = scrolled_y = False
@@ -2231,7 +2239,7 @@ class Widget(DOMNode):
if show_horizontal_scrollbar and show_vertical_scrollbar:
(
- _,
+ window_region,
vertical_scrollbar_region,
horizontal_scrollbar_region,
scrollbar_corner_gap,
@@ -2242,18 +2250,34 @@ class Widget(DOMNode):
if scrollbar_corner_gap:
yield self.scrollbar_corner, scrollbar_corner_gap
if vertical_scrollbar_region:
- yield self.vertical_scrollbar, vertical_scrollbar_region
+ scrollbar = self.vertical_scrollbar
+ scrollbar.window_virtual_size = self.virtual_size.height
+ scrollbar.window_size = window_region.height
+ yield scrollbar, vertical_scrollbar_region
if horizontal_scrollbar_region:
- yield self.horizontal_scrollbar, horizontal_scrollbar_region
+ scrollbar = self.horizontal_scrollbar
+ scrollbar.window_virtual_size = self.virtual_size.width
+ scrollbar.window_size = window_region.width
+ yield scrollbar, horizontal_scrollbar_region
elif show_vertical_scrollbar:
- _, scrollbar_region = region.split_vertical(-scrollbar_size_vertical)
+ window_region, scrollbar_region = region.split_vertical(
+ -scrollbar_size_vertical
+ )
if scrollbar_region:
- yield self.vertical_scrollbar, scrollbar_region
+ scrollbar = self.vertical_scrollbar
+ scrollbar.window_virtual_size = self.virtual_size.height
+ scrollbar.window_size = window_region.height
+ yield scrollbar, scrollbar_region
elif show_horizontal_scrollbar:
- _, scrollbar_region = region.split_horizontal(-scrollbar_size_horizontal)
+ window_region, scrollbar_region = region.split_horizontal(
+ -scrollbar_size_horizontal
+ )
if scrollbar_region:
- yield self.horizontal_scrollbar, scrollbar_region
+ scrollbar = self.horizontal_scrollbar
+ scrollbar.window_virtual_size = self.virtual_size.width
+ scrollbar.window_size = window_region.width
+ yield scrollbar, scrollbar_region
def get_pseudo_classes(self) -> Iterable[str]:
"""Pseudo classes for a widget.
@@ -2374,9 +2398,13 @@ class Widget(DOMNode):
self.vertical_scrollbar.window_size = (
height - self.scrollbar_size_horizontal
)
+ if self.vertical_scrollbar._repaint_required:
+ self.call_next(self.vertical_scrollbar.refresh)
if self.show_horizontal_scrollbar:
self.horizontal_scrollbar.window_virtual_size = virtual_size.width
self.horizontal_scrollbar.window_size = width - self.scrollbar_size_vertical
+ if self.horizontal_scrollbar._repaint_required:
+ self.call_next(self.horizontal_scrollbar.refresh)
self.scroll_x = self.validate_scroll_x(self.scroll_x)
self.scroll_y = self.validate_scroll_y(self.scroll_y)
@@ -2498,6 +2526,7 @@ class Widget(DOMNode):
"""
if layout and not self._layout_required:
self._layout_required = True
+ self._stabilize_scrollbar = None
for ancestor in self.ancestors:
if not isinstance(ancestor, Widget):
break
diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py
index 8416c6f76..fa12226ea 100644
--- a/src/textual/widgets/_text_log.py
+++ b/src/textual/widgets/_text_log.py
@@ -152,7 +152,7 @@ class TextLog(ScrollView, can_focus=True):
self.refresh()
self.lines = self.lines[-self.max_lines :]
self.virtual_size = Size(self.max_width, len(self.lines))
- self.scroll_end(animate=False, speed=100)
+ self.scroll_end(animate=False)
def clear(self) -> None:
"""Clear the text log."""
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 6fd15c510..744398edd 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -8109,132 +8109,132 @@
font-weight: 700;
}
- .terminal-849091649-matrix {
+ .terminal-3799145088-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-849091649-title {
+ .terminal-3799145088-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-849091649-r1 { fill: #c5c8c6 }
- .terminal-849091649-r2 { fill: #3333ff }
- .terminal-849091649-r3 { fill: #14191f }
+ .terminal-3799145088-r1 { fill: #c5c8c6 }
+ .terminal-3799145088-r2 { fill: #3333ff }
+ .terminal-3799145088-r3 { fill: #14191f }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- ScrollbarApp
+ ScrollbarApp
-
-
-
-
-
- I must not fear.
- Fear is the mind-killer.
- Fear is the little-death that brings total obliteration.
- I will face my fear.
- I will permit it to pass over me and through me.
- And when it has gone past, I will turn the inner eye to see its path.
- Where the fear has gone there will be nothing. Only I will remain.
- I must not fear.
- Fear is the mind-killer.
- Fear is the little-death that brings total obliteration.
- I will face my fear.
- I will permit it to pass over me and through me.▂▂▂▂
- And when it has gone past, I will turn the inner eye to see its path.
- Where the fear has gone there will be nothing. Only I will remain.
- I must not fear.
- Fear is the mind-killer.
- Fear is the little-death that brings total obliteration.
- I will face my fear.
- I will permit it to pass over me and through me.
- And when it has gone past, I will turn the inner eye to see its path.
- Where the fear has gone there will be nothing. Only I will remain.
+
+
+
+
+
+ I must not fear.
+ Fear is the mind-killer.
+ Fear is the little-death that brings total obliteration.▁▁▁▁
+ I will face my fear.
+ I will permit it to pass over me and through me.
+ And when it has gone past, I will turn the inner eye to see its path.
+ Where the fear has gone there will be nothing. Only I will remain.
+ I must not fear.
+ Fear is the mind-killer.
+ Fear is the little-death that brings total obliteration.
+ I will face my fear.
+
+
+
+
+
+
+
+
+
+
@@ -11162,163 +11162,162 @@
font-weight: 700;
}
- .terminal-1429392340-matrix {
+ .terminal-52139843-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-1429392340-title {
+ .terminal-52139843-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-1429392340-r1 { fill: #c5c8c6 }
- .terminal-1429392340-r2 { fill: #e3e3e3 }
- .terminal-1429392340-r3 { fill: #313437 }
- .terminal-1429392340-r4 { fill: #324f70 }
- .terminal-1429392340-r5 { fill: #4f9262 }
- .terminal-1429392340-r6 { fill: #a4823a }
- .terminal-1429392340-r7 { fill: #904354 }
- .terminal-1429392340-r8 { fill: #e1e1e1 }
- .terminal-1429392340-r9 { fill: #7c7d7e;font-weight: bold }
- .terminal-1429392340-r10 { fill: #75828b;font-weight: bold }
- .terminal-1429392340-r11 { fill: #192e1f;font-weight: bold }
- .terminal-1429392340-r12 { fill: #3a2a13;font-weight: bold }
- .terminal-1429392340-r13 { fill: #978186;font-weight: bold }
- .terminal-1429392340-r14 { fill: #101011 }
- .terminal-1429392340-r15 { fill: #0c1e39 }
- .terminal-1429392340-r16 { fill: #156034 }
- .terminal-1429392340-r17 { fill: #825210 }
- .terminal-1429392340-r18 { fill: #5b132a }
- .terminal-1429392340-r19 { fill: #7b7b7b }
- .terminal-1429392340-r20 { fill: #3a2a13 }
- .terminal-1429392340-r21 { fill: #78838b }
- .terminal-1429392340-r22 { fill: #7f8081 }
- .terminal-1429392340-r23 { fill: #7c7d7e }
- .terminal-1429392340-r24 { fill: #31220c;font-weight: bold }
- .terminal-1429392340-r25 { fill: #e2e3e3 }
- .terminal-1429392340-r26 { fill: #104e2d }
- .terminal-1429392340-r27 { fill: #7a7b7b }
- .terminal-1429392340-r28 { fill: #1c1c1c }
- .terminal-1429392340-r29 { fill: #191919 }
- .terminal-1429392340-r30 { fill: #181818 }
- .terminal-1429392340-r31 { fill: #7c7c7c }
- .terminal-1429392340-r32 { fill: #494949 }
- .terminal-1429392340-r33 { fill: #14191f }
- .terminal-1429392340-r34 { fill: #ddedf9 }
+ .terminal-52139843-r1 { fill: #c5c8c6 }
+ .terminal-52139843-r2 { fill: #e3e3e3 }
+ .terminal-52139843-r3 { fill: #313437 }
+ .terminal-52139843-r4 { fill: #324f70 }
+ .terminal-52139843-r5 { fill: #4f9262 }
+ .terminal-52139843-r6 { fill: #a4823a }
+ .terminal-52139843-r7 { fill: #904354 }
+ .terminal-52139843-r8 { fill: #7c7d7e;font-weight: bold }
+ .terminal-52139843-r9 { fill: #75828b;font-weight: bold }
+ .terminal-52139843-r10 { fill: #192e1f;font-weight: bold }
+ .terminal-52139843-r11 { fill: #3a2a13;font-weight: bold }
+ .terminal-52139843-r12 { fill: #978186;font-weight: bold }
+ .terminal-52139843-r13 { fill: #101011 }
+ .terminal-52139843-r14 { fill: #0c1e39 }
+ .terminal-52139843-r15 { fill: #156034 }
+ .terminal-52139843-r16 { fill: #825210 }
+ .terminal-52139843-r17 { fill: #5b132a }
+ .terminal-52139843-r18 { fill: #7b7b7b }
+ .terminal-52139843-r19 { fill: #e1e1e1 }
+ .terminal-52139843-r20 { fill: #3a2a13 }
+ .terminal-52139843-r21 { fill: #78838b }
+ .terminal-52139843-r22 { fill: #7f8081 }
+ .terminal-52139843-r23 { fill: #7c7d7e }
+ .terminal-52139843-r24 { fill: #31220c;font-weight: bold }
+ .terminal-52139843-r25 { fill: #e2e3e3 }
+ .terminal-52139843-r26 { fill: #104e2d }
+ .terminal-52139843-r27 { fill: #7a7b7b }
+ .terminal-52139843-r28 { fill: #1c1c1c }
+ .terminal-52139843-r29 { fill: #191919 }
+ .terminal-52139843-r30 { fill: #181818 }
+ .terminal-52139843-r31 { fill: #7c7c7c }
+ .terminal-52139843-r32 { fill: #494949 }
+ .terminal-52139843-r33 { fill: #ddedf9 }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- WidgetDisableTestApp
+ WidgetDisableTestApp
-
-
-
- ⭘WidgetDisableTestApp
- ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
- ButtonButtonButtonButtonButton
- ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
- Column 1 Column 2 Column 3 Column 4
- 0 0 0 0
- This is list item 0
- This is list item 1
- ▼ This is a test tree
- ├── Leaf 0
- Hello, World!
-
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊This is an empty input with a placeholder▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
- ▊This is some text in an input▎
- ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
- ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
- ▇▇
+
+
+
+ ⭘WidgetDisableTestApp
+ ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
+ ButtonButtonButtonButtonButton
+ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
+ Column 1 Column 2 Column 3 Column 4
+ 0 0 0 0
+ This is list item 0
+ This is list item 1
+ ▼ This is a test tree
+ ├── Leaf 0
+ Hello, World!
+
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊This is an empty input with a placeholder▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▊▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▎
+ ▊This is some text in an input▎
+ ▊▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▎
+ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
+
@@ -13524,6 +13523,162 @@
'''
# ---
+# name: test_line_api_scrollbars
+ '''
+
+
+ '''
+# ---
# name: test_list_view
'''