Skip to content

fix(ui): let a canvas be sized without collapsing its panel - #106

Merged
nicolas-maman merged 2 commits into
mainfrom
fix/canvas-panel-collapse
Sep 4, 2026
Merged

fix(ui): let a canvas be sized without collapsing its panel#106
nicolas-maman merged 2 commits into
mainfrom
fix/canvas-panel-collapse

Conversation

@nicolas-maman

Copy link
Copy Markdown
Contributor

Root cause

Canvas ids and widget handles are separate counters that both start at 1.
canvas_create returns a canvas id; width / height / fill_width / style
take a widget handle. Neither space rejects the other's numbers, so passing one
where the other belongs does something, silently, to an unrelated widget.

That is exactly what the report ran into:

g_swatch = ui.canvas_create(swatch_row, 46, 14)
ui.width(g_swatch, 46)     // pins WIDGET number <canvas id> to 46px

The chip was the second canvas, so its id was 2, and widget 2 in that editor
was the inspector panel.

Reproduced

A three-pane layout here, reading widths back with get_width:

left centre right
no chip 59 1025 116
chip, sized with width(canvas_id, 46) 0 0 46
chip, sized with canvas_size(chip, 46, 14) 59 1025 116

The panel collapses to the chip's width, and in this layout the other two panes
go to 0 as well: the whole layout, not one panel. This also explains why the
reporter could not reduce it. Which widget the id collides with depends on
construction order, so it only shows up in a layout big enough for the
collision to land somewhere that matters, and the symptom, a panel sized like
the canvas, points at the canvas rather than at the id.

Fix

canvas_size(id, w, h) takes the canvas id the caller is holding and does the
conversion itself, so the natural call is the correct one. canvas_create's
width and height are only a natural size (priority 150, so a canvas can
absorb a stack's slack), which is why a chip needs an explicit one at all.

canvas_create and canvas_widget now state in the API docs that the two
numbering spaces overlap, that neither rejects the other's numbers, and that
canvas_widget is the whole of the boundary between them.

What this deliberately does not do

It does not renumber the id spaces to make the mix-up impossible. That is the
fix that would remove the trap entirely rather than paving over it, and it is
not something I could verify from here:

  • 56 spec routes address canvases by their small wire id (/canvas/1/click),
    so the wire numbering has to stay.
  • AppKit and GTK4 each funnel every canvas call through one lookup, but win32
    open-codes the same bounds check in 18 places, and some internal callers pass
    ids that are already normalized. A blind offset there would double-subtract
    somewhere and I can only compile-check win32 from this machine.

So the trap is removed for the case that actually bit, and the overlap is
documented at both ends rather than left to be rediscovered.

Verification

  • aeb .all.ae builds clean, zero warnings.
  • panelcanvas_demo reports right=116 chip=46: the panel keeps its width and
    the chip gets exactly the size it asked for.
  • spec_panelcanvas_demo asserts the panel's rows are not squeezed into the
    chip and that all three panes still have a width. Wired into ci.sh as phase
    5e20, so it runs on AppKit and GTK4.

Closes #98

🤖 Generated with Claude Code

nicolas-maman and others added 2 commits September 4, 2026 18:20
Canvas ids and widget handles are separate counters that both start at 1,
so the same small integer means different things to canvas_* and to the
widget functions, and passing one where the other belongs is silent.

width(canvas_create(...), 46) therefore pins WIDGET number <canvas id> to
46px. In a three-pane editor that was the inspector panel: it collapsed to
roughly the chip's own width and every child reflowed into it. Reproduced
here in a three-pane layout, where the wrong call left the panel at 46 and
the other two panes measuring 0, and the right call leaves the panel at its
full 116 with the chip at exactly the 46 it asked for. Nothing in that
picture points at the real mistake, which is why the report reads as the
canvas acting as a strut on its parent, and why it could not be reduced:
which widget the id collides with depends on construction order, so it only
appears in a layout big enough for the collision to land somewhere that
matters.

canvas_size(id, w, h) takes the id the caller is holding and does the
conversion itself, so the natural call is the correct one. canvas_create and
canvas_widget now say plainly that the two numbering spaces overlap, that
neither rejects the other's numbers, and that canvas_widget is the whole of
the boundary between them.

The numbering itself is left alone on purpose: 56 spec routes address
canvases by their small wire id, and win32 open-codes the same bounds check
in 18 places with internal callers that already pass normalized ids, so
renumbering there could not be verified from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nicolas-maman
nicolas-maman merged commit 3257547 into main Sep 4, 2026
3 checks passed
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.

A second canvas in a side panel collapses that panel to the canvas width

1 participant