Skip to content

fix(gui): stop the settings scroller leaving its content, and fill the table when columns are hidden - #130

Merged
donislawdev merged 5 commits into
masterfrom
fix/settings-scroller-and-column-fit
Aug 17, 2026
Merged

fix(gui): stop the settings scroller leaving its content, and fill the table when columns are hidden#130
donislawdev merged 5 commits into
masterfrom
fix/settings-scroller-and-column-fit

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Two reported interface faults, and three things found while fixing them.

Reported

The Settings window scrolled off the top of itself. Dragging its scrollbar upwards pushed
everything down and left a blank band above the first setting.

Cause, measured on real Tk: viewport 855 px, content 452 px, region 0 0 807 452. Tk holds a view
inside its region only while the region is TALLER than the window - with a shorter region the canvas
moves its origin above the content, and yview keeps reporting (0.0, 1.0) while it does. One
scroll-up left canvasy(0) at -360. After: the region reads 0 0 807 855 and the same scroll
leaves it at 0.0.

The wheel never showed this because ScrollableFrame.scroll() guards on can_scroll(), while the
scrollbar is wired straight to canvas.yview and never passed through that guard. Clamping the
region instead of duplicating the guard covers the wheel, the scrollbar and any future caller at
once.

Hiding columns left the Connections table half empty. The table refuses ttk stretch
deliberately - ttk recomputes a stretch column on the next <Configure> and a dragged width visibly
snaps back - and the cost of that decision only shows once columns can be hidden. Measured in a
1076 px tree: 17 columns sum to 1611 px, so the horizontal scrollbar has work and that is correct,
but 2 columns sum to 205 px and leave 871 px of bare background.

fitted_widths() carries four rules, each with its reason: only ever WIDEN, so a width the user
dragged to is never taken back; share the slack in proportion to natural width, so a column born
wide stays the wide one; cap each column at three times its natural width, the same ceiling a drag
gets; and when the caps bind, leave the remainder rather than break one. The memo (_fitted_for) is
a rule too, not an optimisation - a fit is asked for when the visible set changes or the widget is
resized, and a column drag changes neither, so the chosen width stands.

Measured after: 4 columns, the reported case, take the full width; 2 columns leave 461 px and 1
leaves 674 px, every column standing on its cap.

Found on the way

An assertion that could not fail, inside the guard that guards the guards.
test_the_repository_scanners_stay_out_of_what_is_not_in_the_repository asserted that a maintainer
brief is not scanned - by naming a file deleted in August. The scanner could never have returned it
whatever the skip list held, so emptying that list changed nothing and the guard still read as
proof. Only the mutation runner said otherwise. The test now plants a brief for the length of the
scan, so the skip has something to skip, and the mutation is caught.

The language files were not sorted, and nothing checked. One pair was out of order in both files
identically, and the same pair is out of order in the merge base, so it predates this work. Swapped
at byte level, because rewriting a text file on Windows turns LF into CRLF and the repository is LF.
The new guard was shown able to fail before being believed.

The row limit said the same thing twice - 50000 rows (0 = off) 0 = no limit. Checked rather
than assumed: the two other fields with an "(0=off)" unit have no hint to carry it, so row_limit
was the only one with both. The unit is now bare.

Verification

  • Full suite 1134 passed, plus the GUI smoke script.
  • Every mutation in the registry caught: 102, survived 0, with the deliberately unparsable canary
    reporting BROKEN as it must. The registry had one survivor before this branch.
  • Both fixed surfaces captured live from real Tk with PrintWindow, not reasoned about: the settings
    window sits flush at the top after a scroll-up, and the four columns span the table.
  • No regression on the Control page, whose content IS taller than its viewport: its region stays at
    the content height, scrolling down works and scrolling up clamps at 0.

Said out loud rather than left to be discovered

  • "Slack 0" is measured against winfo_width(), and a Treeview's usable width is 4 px less -
    2 px of border a side, constant, checked at two different widths. So xview reads 0.9963 and the
    horizontal scrollbar stays technically active over those 4 px. Not corrected by subtracting 4: that
    constant belongs to the theme and the platform and cannot be checked on the Linux runner, which is
    exactly the class of thing a Windows-only check waves through. There is no visible difference.
  • With one or two narrow columns shown, the cap leaves real space over. That is the agreed behaviour,
    not an oversight - lifting it would mean a fit reaching a width no drag could.
  • A suspicion the measurement killed, recorded so nobody re-opens it: a neighbouring loop looked like
    the same vacuous-assertion fault, comparing path prefixes with a backslash against paths normalised
    to forward slashes. It is not - the source uses forward slashes, and patching the skip list by hand
    reddens that loop correctly with 65 leaked files. Nothing was changed there.
  • Duplicate-looking rows with columns hidden are by design and were left alone: the columns that tell
    those rows apart are the hidden ones.

donislawdev and others added 5 commits August 17, 2026 23:11
…ave the content

Reported: dragging the Settings window scrollbar upwards pushed everything down
and left a blank band above the first setting.

Cause, measured on real Tk: viewport 855 px, content 452 px, scroll region
"0 0 807 452". Tk holds a view inside its region only while the region is TALLER
than the window - with a shorter region the canvas moves its origin above the
content, and yview keeps reporting (0.0, 1.0) while it does. One scroll-up put
canvasy(0) at -360. After the fix the region reads "0 0 807 855" and the same
scroll leaves canvasy(0) at 0.0.

The wheel never showed this because ScrollableFrame.scroll() guards on
can_scroll(), while the scrollbar is wired straight to canvas.yview and never
passed through that guard. Fixing the region instead of duplicating the guard
covers the wheel, the scrollbar and any future caller at once.

A second fault in the same lines, found while fixing the first: bbox("all")
returns None on an empty canvas and configure(scrollregion=None) CLEARS the
region, which is the same unconfined canvas by another route.

- clamped_scrollregion() is pure, because the fake tkinter canvas has no
  canvasy/bbox/scrollregion and cannot express a drifted origin - so the guard
  is on the arithmetic, and the test says why
- both callers now share _apply_scrollregion; the canvas handler clamps to
  event.height, since winfo_height can still report the old size mid-resize
- four tests in test_wheel_and_scroll.py, two MUTATIONS entries, both confirmed
  caught by internal_tools/mutate.py
- verified no regression on the Control page, whose content IS taller than its
  viewport: region unchanged at "0 0 493 1199", down still scrolls, up clamps

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reported: hiding columns in Connections left the remaining ones huddled on the
left with bare background beside them.

Cause: this table refuses ttk stretch deliberately, because ttk recomputes a
stretch column on the next <Configure> and a dragged width visibly snaps back.
The cost of that decision only shows once columns can be hidden - nothing was
absorbing the space they left. Measured on real Tk in a 1076 px tree: 17 columns
sum to 1611 px, so the horizontal scrollbar has work and that is correct, but 2
columns sum to 205 px and leave 871 px of dead space.

fitted_widths() is pure and carries four rules, each with its reason:

- only ever WIDEN, so a width the user dragged to is never taken back - the very
  property that refusing stretch was protecting
- share the slack in proportion to NATURAL width, so a column born wide stays
  the wide one instead of a PID column growing as wide as a path
- cap each column at MAX_WIDTH_FACTOR x natural, the same ceiling clamp_widths
  applies after a drag, so a fit cannot reach a width a drag could not
- when the caps bind, leave the remainder as dead space rather than break one

Measured after: 17 columns unchanged; 4 columns - the reported case - fill
exactly, slack 0; 2 columns leave 461 px and 1 leaves 674 px, every column
standing on its 3x cap. The residue is the cap working, and only one or two
narrow columns can reach it.

The memo (_fitted_for) is a rule rather than an optimisation: a fit is asked for
when the visible set changes or the widget is resized, and a column drag changes
neither, so the chosen width stands. Verified live - narrowing a column to 90 px
and calling fit_columns() leaves it at 90.

- seven tests in test_virtual_tables.py, six on the pure function and one on the
  memo through the fake tkinter, whose Treeview.column does work as a getter
  (checked first, so the assertion can actually fail)
- three MUTATIONS entries, all three confirmed caught

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The field rendered "50000 rows (0 = off)   0 = no limit" - the unit and the hint
both explaining what zero does, which is convention 1b broken in the smallest
possible way.

Checked rather than assumed: max_size (fields.unit_b_off) and nat_timeout
(fields.unit_s_off) carry the "(0=off)" inside their unit precisely because they
have NO hint to carry it. row_limit was the only field in the registry with
both, so the convention for the others is left alone.

- fields.py points row_limit at a bare fields.unit_rows
- fields.unit_rows_off removed from both language files; its only consumer was
  fields.py, verified across the whole tree with zero hits in tests/ or site/
- renders as "Row limit: [50000] rows   0 = no limit" and
  "Limit wierszy: [50000] wierszy   0 = bez limitu"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_the_repository_scanners_stay_out_of_what_is_not_in_the_repository asserted
that HANDOFF-UI-CLI.md is not scanned - a brief deleted on 2026-08-10.
repo_text_files could never return it whatever SKIP_PREFIXES held, so emptying
that tuple changed nothing and the guard still read as proof. The only thing
that said otherwise was internal_tools/mutate.py, reporting SURVIVED for
"guards: a HANDOFF brief falls back into the scanned set".

The test now plants a HANDOFF-scanner-probe.md for the length of the scan, so
the skip has something to skip, and removes it in a finally. The name is covered
by the existing HANDOFF-*.md ignore rule, so the tree stays clean, and the test
refuses to run rather than overwrite a file of that name that already exists.
Its content carries an em-dash built with chr(0x2014): a literal one is
impossible here, because this file is repository text and is scanned by the very
rule it defines.

Measured after: the mutation is caught.

A suspicion the measurement killed, recorded so nobody re-opens it: the
neighbouring loop looked like the same class of fault, comparing prefixes with a
backslash against paths normalised to forward slashes. It is not - the source
uses forward slashes and, checked by patching SKIP_DIRS by hand, that loop
reddens correctly with 65 leaked files. Nothing was changed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note has claimed lang/*.json are sorted since they existed and nothing
checked it. One pair was out of order in both files identically -
log.driver_wait before log.driver_still_unloading - and the same pair is out of
order in HEAD, so this predates the current work.

Swapped at byte level rather than by rewriting the JSON: rewriting a text file on
Windows turns LF into CRLF, the repo is LF by .gitattributes, and the tree then
looks dirty for a reason the diff does not explain. Verified after: one line
changed per file, zero CRLF, both files still parse and hold exactly the same
keys and values.

test_the_language_files_stay_sorted also checks _meta comes first, which needs no
exception because it sorts before every dotted key on its own.

The guard was shown able to fail before being believed: swapping the pair back
out of order turns it red, restoring it turns it green. Filed in PROVEN_BY_HAND
rather than MUTATIONS, because a re-runnable patch would mean pasting the two
enormous log.driver_* lines twice into the registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 84b03f9 into master Aug 17, 2026
8 checks passed
@donislawdev
donislawdev deleted the fix/settings-scroller-and-column-fit branch August 17, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant