Skip to content

kanban(gui): the board was 3px wide and its right-hand columns unreachable — measured, fixed, and the fix mutation-tested - #794

Merged
Yaraslaut merged 4 commits into
masterfrom
fix/kanban-board-unreachable
Sep 24, 2026
Merged

Yaraslaut merged 4 commits into
masterfrom
fix/kanban-board-unreachable

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

The repository owner reported that the kanban board is not visible and that
Activity overlaps it. It reproduces, and it is worse than "too narrow": at the
shipped 1280×860 window the board area measured 3 px and the Activity panel
1237 px.

examples/ is a detector, not a product, so this is fixed in place and nothing
is filed. The cause is entirely the example's own QML — no morph forms/qt layer
behaviour and no framework sizing hint is involved, so there is no framework gap
to file against include/morph either.

The three causes, each confirmed

Confirmed how Before After
No scroll container. grep -nE "ScrollView|Flickable|contentWidth|ScrollBar" BoardView.qml returned nothing; the test's findItem(boardArea, "boardFlickable") was nullptr. grep + measured columns past the edge drawn beyond clip: true, with nothing to reach them strip in a Flickable with an always-on horizontal ScrollBar
No floor on boardRoot. grep -nE "Layout\.(minimumWidth|maximumWidth)" returned nothing across every kanban QML file. grep + measured board area 3 px Layout.minimumWidth = one column + gaps (256)
Unbounded Activity panel. A ColumnLayout nested in a RowLayout fills width by default — Layout.preferredWidth: 280 does not stop it — and its implicit width is the widest unwrapped activity summary. measured activity 1237 px fillWidth: false, 200…320 band → 280

Measured widths at the default window size, from the test's own INFO:

before: window 1280, board area 3,   activity 1237
after:  window 1280, board area 960, activity 280
        narrow window 500, board area 256, activity 280

960 is four columns short of five (1232) — which is why reachability, not just
width, is the property that matters.

The test, and the mutations that turn it red

examples/kanban/tests/test_board_layout.cpp runs offscreen
(QT_QPA_PLATFORM=offscreen). It loads Main.qml with both bridges attached
rather than BoardView as a bare root, so the widths it measures are the ones
the real shell produces, and reaches the board by evaluating
stack.push(boardPage) in Main's own context — verbatim the statement
onProjectOpened runs, so no input event is synthesized (presenter rule 6). Five
columns are created through BoardBridge::createColumn.

It asserts the sidebar stays a sidebar; the board holds at least one whole
column; the strip is genuinely wider than the area it is drawn in (so the
reachability check cannot pass vacuously); the scrollbar is on screen; and that
the last column — which starts outside the viewport — is wholly inside it after
scrolling to the end. The floor is then re-checked at a 500 px window, the only
size at which it, and not the sidebar's bound, is what decides the split.

Each of the three fixes was removed in turn and the test re-run:

remove the Flickable      REQUIRE( flick != nullptr )  →  nullptr != nullptr
drop boardRoot's floor    CHECK( boardArea->width() >= 256 )  →  180.0 >= 256.0
                          (narrow window 500, board area 180, activity 280)
drop the sidebar's bound  CHECK( activityPanel->width() <= 320 )  →  648.0 <= 320
                          (window 1280, board area 592, activity 648)

Other views

Checked ProjectListView.qml, MembersView.qml, RulesView.qml,
TaskDetailPopup.qml, LoginView.qml. None has this defect, and none is
changed here.

ProjectListView.qml's two-pane row has the same declaration shape — pane 1
is a nested ColumnLayout with Layout.preferredWidth: 420 and no
fillWidth: false, so both panes fill and split the row roughly evenly instead
of honouring the 420. It is not the same defect: neither pane holds
fixed-width content, nothing is clipped, and nothing becomes unreachable.
Pinning pane 1 to 420 would also leave dead space whenever the members pane is
hidden (it is visible: false until a project is selected), so it is left
alone deliberately rather than by omission. Everything else on these screens is
elastic — labels elide, list delegates take their view's width, forms wrap — and
the vertical ListViews are already flickable.

Gates

  • clang-format --dry-run -Werror (22.1.8) over the one changed C++ file — clean.
  • ctest -N -L ladder-kanban selects 182 tests (181 before, +1 here), so the
    filter is not matching nothing.
  • ctest -L ladder-kanban -j6 → 100% tests passed out of 182, including both
    pre-existing QML engine-load smoke cases (no new QML warnings — the suite fails
    on any).
  • ladder_kanban_qml_qmllint: 19 warnings before, 19 after, none of them in
    the changed block.
  • No C++ header touched, so no Doxygen run.

Build: Clang 22.1.8, Release, MORPH_BUILD_LADDER=ON MORPH_LADDER_RUNGS=kanban MORPH_BUILD_QT=ON MORPH_BUILD_FORMS_QML=ON.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW

Yaraslaut and others added 4 commits September 24, 2026 02:42
…hable

At the shipped 1280x860 window the board area measured 3 pixels and the
Activity panel 1237. Three things compounded:

The Activity panel is a ColumnLayout inside a RowLayout, and a layout
nested in a layout fills width by default -- `Layout.preferredWidth: 280`
does not stop it. Its implicit width is the widest unwrapped activity
summary, so it took the whole row. It is now `fillWidth: false` with a
200..320 band, which puts it at 280 and leaves the board 960.

`boardRoot` is a bare Item, implicit width 0, with `fillWidth: true` and
no floor, so there was nothing to stop that. It now carries a
`Layout.minimumWidth` of one column plus the gaps either side -- below
that the first column is cut in half and no window size rescues it.

The columns were laid out in a bare Row inside an Item with `clip: true`
and no scroll container anywhere in the file, so a strip wider than the
viewport was not merely cut off but unreachable: a four-column board
already needs 984 against the 960 it gets. The strip now lives in a
Flickable with an always-on horizontal ScrollBar, sized from the column
count rather than from the Row's implicit width, since a Row is a
positioner and stretching it moves nothing inside it.

The two numbers the strip is built from -- 240 wide, 8 apart -- are now
named once on `boardRoot` and read by the delegate, the Row's spacing,
the floor and the content width.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
A layout that "looks right" is the weakest evidence there is, and an
offscreen run cannot look at anything -- so this measures instead.

It loads Main.qml with both bridges attached, so the widths under test
are the ones the real shell produces (the 1280x860 window, its margins,
the StackView, BoardView's own margins), and reaches the board by
evaluating `stack.push(boardPage)` in Main's own context -- verbatim the
statement its `onProjectOpened` handler runs, so no input event is
synthesized. Five columns are created through the bridge's typed entry
point, which is 1232px of strip against a 960px board area.

Asserted: the Activity panel stays within a sidebar's width; the board
area holds at least one whole column; the strip is genuinely wider than
the area (otherwise the reachability check would prove nothing); a
horizontal scrollbar is on screen; and the last column, which starts
outside the viewport, is wholly inside it once scrolled to the end. The
floor is then checked again at a 500px window, the only size at which it
is what decides the split rather than the sidebar's own bound.

Each of the three fixes was removed in turn against this test:
dropping the Flickable leaves it unfound; dropping the floor puts the
board at 180 of the 256 it needs at 500px; dropping the sidebar's bound
puts Activity at 648.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
The layout test landed without the clang-tidy gate having been run over it, so
three findings on changed lines reached CI:

    readability-identifier-length   `id` is shorter than the three-character floor
    misc-no-recursion               `findItem` calls itself
    (precedence)                    `a + 2 * b` wants the multiplication bracketed

The first and third are renames and a pair of brackets. The second is not: the
function walks a QQuickItem tree of unbounded depth, and an iterative version
would hand-roll the same stack with none of the clarity, so it carries a
suppression that says why rather than a rewrite that pretends the shape is
different.

No assertion, bound or measured value changes. The test still fails against
each of the three mutations it was written to catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
… builds its bounds from

Three further `readability-math-missing-parentheses` findings on changed lines,
in the two expressions that compute the strip extent and the minimum usable
board width. The earlier pass fixed one occurrence and did not look for the
rest, which is why this is a second commit rather than an amendment.

    (kColumnCount * kColumnWidth) + ((kColumnCount - 1) * kColumnSpacing)
    kColumnWidth + (2 * kColumnSpacing)

Brackets only -- the operator precedence C++ already applies is what is now
written down, so every bound the test asserts keeps its value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VptDWG2fKr2vBnLSJcgzgW
@Yaraslaut
Yaraslaut force-pushed the fix/kanban-board-unreachable branch from 7771063 to 57a5c3f Compare September 24, 2026 00:42
@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit 8154f5f into master Sep 24, 2026
37 checks passed
@Yaraslaut
Yaraslaut deleted the fix/kanban-board-unreachable branch September 24, 2026 04:42
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