textual 8.2.8
Anchoring a scrollable container (widget.anchor()) whose content is shorter than
its viewport drives scroll_y negative and keeps it there. Visually: the content
hugs the bottom edge of the container with a void above it, and the state sticks
rather than flashing, because it is re-applied on every arrange.
Minimal repro
from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.widgets import Static
class AnchorVoid(App):
def compose(self) -> ComposeResult:
with VerticalScroll(id="v"):
for i in range(3): # far less than one screen
yield Static(f"line {i}")
def on_mount(self) -> None:
self.query_one("#v", VerticalScroll).anchor()
self.set_interval(0.5, self.report)
def report(self) -> None:
v = self.query_one("#v", VerticalScroll)
self.log(f"scroll_y={v.scroll_y} max_scroll_y={v.max_scroll_y}")
if __name__ == "__main__":
AnchorVoid().run()
Run it in a normal-height terminal: the three lines render at the bottom of the
screen with emptiness above, and the log shows a persistent negative scroll_y
(for example scroll_y=-47.0 max_scroll_y=0).
In _compositor.py, the anchored branch of the arrange pass computes:
if widget._anchored and not widget._anchor_released:
new_scroll_y = (
arrange_result.spatial_map.total_region.bottom
- (widget.container_size.height - widget.scrollbar_size_horizontal)
)
widget.set_reactive(Widget.scroll_y, new_scroll_y)
widget.set_reactive(Widget.scroll_target_y, new_scroll_y)
Content-bottom minus viewport-height is negative whenever the content is shorter
than the viewport, and the write goes through set_reactive, which bypasses the
reactive's validation - so the clamp that protects every other scroll path
(validate_scroll_y flooring at 0) never runs. Because the branch executes on
every arrange, the negative value is re-imposed even if something resets it.
An anchored container whose content fits the viewport should rest at
scroll_y == 0 (there is nothing to stay at the bottom of), and the anchored
arithmetic should be clamped to the same floor the validated scroll paths get -
e.g. max(0, ...) around the computed offset, or routing the write through
validation.
textual 8.2.8
Anchoring a scrollable container (
widget.anchor()) whose content is shorter thanits viewport drives
scroll_ynegative and keeps it there. Visually: the contenthugs the bottom edge of the container with a void above it, and the state sticks
rather than flashing, because it is re-applied on every arrange.
Minimal repro
Run it in a normal-height terminal: the three lines render at the bottom of the
screen with emptiness above, and the log shows a persistent negative
scroll_y(for example
scroll_y=-47.0 max_scroll_y=0).In
_compositor.py, the anchored branch of the arrange pass computes:Content-bottom minus viewport-height is negative whenever the content is shorter
than the viewport, and the write goes through
set_reactive, which bypasses thereactive's validation - so the clamp that protects every other scroll path
(
validate_scroll_yflooring at 0) never runs. Because the branch executes onevery arrange, the negative value is re-imposed even if something resets it.
An anchored container whose content fits the viewport should rest at
scroll_y == 0(there is nothing to stay at the bottom of), and the anchoredarithmetic should be clamped to the same floor the validated scroll paths get -
e.g.
max(0, ...)around the computed offset, or routing the write throughvalidation.