utils/gatelock/docs/DOCS-scrollable-viewport.md
Krzysztof kuhy Rudnicki a10d190b65
Some checks failed
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
design_system tests / test (push) Has been cancelled
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
web_ui tests / test (push) Has been cancelled
Add the shared markdown-naming gate and migrate utils' own docs
Markdown across every repo had accumulated a dozen ad-hoc names
(refactor_claude_todo.md x23, DESIGN_AUDIT_TODO.md, NEXT_SESSION,
HANDOFF, PLAN, SPEC) with nothing distinguishing a live task from a
finished record. An audit found docs asserting work was pending that
shipped months earlier, and one asserting completion while the work
was open.

Four namespaces now: README.md, CLAUDE*.md, DOCS*.md, TODO*.md. Task
files carry the literal "REMOVE ME AFTER FINISH" and are deleted when
the work lands.

Enforced bidirectionally -- a TODO without the marker fails, AND any
file carrying the marker that is not named TODO*.md fails. Without the
reverse rule a task can be renamed into the permanent namespace and
quietly live forever, which is the failure being fixed.

md_naming/ mirrors the file_length topology and imports its tables
(EXCLUDED_DIRS, THIRD_PARTY_REPOS, VENDORED_ANYWHERE, GENERATED_PATTERN)
rather than restating them, so the two gates cannot disagree about the
same file. 71 tests, 100% branch coverage.

stale.py warns on TODOs past 90 days but never fails: no machine can
prove a task is finished, so the honest mechanism is a nudge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
2026-08-24 15:53:12 +02:00

2.7 KiB

The scrollable lock viewport

Design rationale for gatelock/_scrollable.py and its collaborators (_scrollfit.py, _scrollkeys.py, _userintent.py). Kept here rather than in the module docstring so the module itself stays readable in one piece; the behaviour it describes is load-bearing, not historical.

A keyboard-scrollable, focus-following viewport for lock-surface content.

Lock surfaces cannot solve "content taller than the screen" the obvious way. A scrollable dialog would be a second Toplevel, and on an override-redirect lock surface a second toplevel steals the Tk grab, which the recovery tick then kills a second later -- the 2026-07-26 failure where a frozen sport selector logged a walk as table tennis. So the viewport has to live inside the surface's own widget tree, which is what this provides.

Three defects this exists to prevent, all of them Tk defaults rather than oversights in any one app:

  1. Centered content clips symmetrically. A place-centered frame takes its requested size, so when it is too tall the parent shears equal amounts off the top and the bottom -- losing the header and the submit button together, with no scrollbar and no hint anything is missing. This top-anchors content inside a scroll region instead.
  2. A Canvas viewport is not keyboard-scrollable. tk.Canvas has no class-level key bindings at all, so ::tk::FocusOK rejects it as a focus stop and the only way to move the view is dragging the scrollbar thumb with a pointer. This makes the canvas focusable and binds PageUp/PageDown, Home/End and arrows.
  3. Focus walks into invisible widgets. Canvas clipping does not unmap a child, so every scrolled-out-of-view field stays winfo viewable and stays in the tab chain, while Tk never scrolls to follow focus. Tabbing therefore lands on fields the user cannot see and cannot bring into view. :meth:ScrollableSurface.scroll_into_view fixes that, but only for focus the user moved.

Wheel scrolling is bound too, but as an addition -- never as the only path.

Scrolling is the fallback, not the design. A lock screen is supposed to fit its screen; content that overflows is a layout defect, and :meth:finalize logs it at warning so it gets fixed rather than lived with. While the content fits -- the normal case -- the scroll region is pinned to the viewport and the scrollbar is hidden, so the view cannot move at all. When it does overflow, the view still only moves in response to a keypress, a click or the wheel: an earlier version followed programmatic focus too, and since the apps re-focus a widget on every repaint, the screen scrolled itself to mid-content and back while the user was doing nothing (reported 2026-08-03, 1366x768).