Skip to content

The strip becomes a panel, and the rest of the page verbs arrive - #9

Merged
tannevaled merged 2 commits into
mainfrom
verbs-panel
Aug 27, 2026
Merged

The strip becomes a panel, and the rest of the page verbs arrive#9
tannevaled merged 2 commits into
mainfrom
verbs-panel

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

The strip held nine things. The library behind it holds about twenty-five, and
most of what is left cannot be a button at all: it has to be told which pages,
or how many to a sheet, or what to write, and a row of buttons has nowhere to
say it. A tenth button already reached two thirds of the way across.

The design decision, and why

The verbs are grouped, and a group opens a panel beside the page rather
than instead of it.

Beside is the whole of it. Every verb here changes the document, and this
workbench draws the document by writing it out and reading it back — so typing
a crop box and watching the page come back cropped is what the control is
for. A panel over the page (which is what Fill in does, correctly, because
a hundred fields need the room) would hide the one thing worth looking at. The
page is rescaled to what the panel leaves, so it shrinks rather than being
pushed off the edge.

The alternative considered and rejected was tabs or a segmented switcher: both
truncate their labels, and Pages and Sheet would both be reduced to an S —
the same reason the strip is buttons rather than a Toolbar.

What is now reachable

Strip: Open · Save · < · > · Rotate · Delete · Pages · Sheet · Marks · File ·
Fill in
. What stays on it is what needs no telling.

group verbs
Pages keep only a range · delete a range · turn a quarter/half/three quarters · reverse · move the page on the screen · crop to a box · insert a blank page · split into files of n
Sheet n to a sheet · booklet · add another file after this one · lay another file over it
Marks watermark, in words somebody chose
File sanitize

Two up, Watermark and Sanitize leave the strip for the group each belongs to.
Two up becomes a number, so it is also three up and sixteen up.

Deliberately left out of this PR

number, bates, stamp, compress, flatten, encrypt, decrypt,
permissions, info, text, images — a second PR, so that this one is
about the shape the panel takes and that one is about filling it. fill is
already done and is not touched. merge/overlay are here because they need
nothing but a second file; split is here because it needs only a number.

Three things that were dead, and one in the toolkit

None of this could work without them, and none was visible before there was a
panel to notice it in.

  1. A press arrived in surface coordinates. A container reads the point it
    is given as local to itself and adds its own origin back on before
    hit-testing. On a strip 8 px from the top that is a near miss the sweeping
    test papered over; on a panel that starts 46 px down it is a press on the
    wrong row.
  2. The view got no events at allhandleClick reached only the toolbar.
    The Fill in panel, real widgets bound through observables, could not be
    pressed in a browser. It was a picture of controls.
  3. Char() returned false, always. Nothing could be typed anywhere, so
    the 76.3% of form fields that are text had no way in.
  4. ScrollView did not forward a press to its child in toolkit v0.250.0,
    so every control in a scrolling panel was dead to the mouse. Fixed upstream
    in v0.267.0; this bumps the toolkit to v0.272.0 for it.

A gap that is not fixed, named rather than worked around

Neither ScrollView nor FormField implements the toolkit's unexported
focusableChildren, so its own focus walk stops at the scroll view and cannot
find a box inside one. The workbench therefore hands a keystroke straight to
the box it knows was last pressed — which works, and is honest, but Tab does
not walk the panel
. That belongs upstream, not here.

What was measured

Eleven verbs over 400 real forms, taken evenly from the eleven issuing
bodies
in /Users/Shared/pdfforms — 400 of the 1 633 real documents, the
635 vendor fixtures excluded. 4 400 outputs, each written by ops, read
back by our own reader, parsed by pdfinfo and drawn by pdftoppm
with ink counted on the page:

4 311 of 4 311 applicable   written, read back, parsed and drawn with ink
   89 not applicable        one-page documents where deleting page one would
                            leave none, which the panel refuses for that reason
poppler drew 4 311 pages, 88 001 381 pixels of ink, 20 413 on average

Two apparent failures in the first run were the measurement's own, and are
worth recording: pdftoppm pads the page number to the width of the page
count, so page-1.png is not always what it writes; and a blank page inserted
at the front is meant to come out blank, so the ink has to be looked for on
page two. Read the output, not the exit code.

In a real browser

The check now presses along the strip from the right until the right-hand band
of the canvas lights up, presses down that panel until one of its verbs changes
the band where the page is, saves, and parses what came back to find
(DRAFT) Tj in it:

a panel opened from x=476
a verb pressed in the panel changed the page: pressed at y=119
the tab handed back a 2537 byte PDF after the panel was used
the mark written from the panel is in the saved file

A panel that is painted and gets no events looks exactly like one that works,
so the pixels are read either side of every press — and the wait before each
look is deliberate: webcanvas repaints on an animation frame, not on the
press.

Checks

100% of statements. Every control in every group is reached by pressing it
where it was drawn
, not by calling its handler. go vet and gofmt clean on
nine targets, -race clean, and the window was rendered and looked at at each
step — which is how the panel was found drawing nothing at all the first time
(a ScrollView keeps its child's own width and height, and nobody had given
the column one).

tannevaled and others added 2 commits August 27, 2026 21:24
The strip held nine things and the library behind it holds about twenty-five.
A tenth button already reached two thirds of the way across, and most of what
is left cannot be a button at all: it has to be told which pages, or how many
to a sheet, or what to write, and a row of buttons has nowhere to say it.

So the verbs are grouped, and a group opens a panel BESIDE the page rather
than instead of it. Beside is the whole design decision. Every verb here
changes the document, and this workbench draws the document by writing it out
and reading it back — so typing a crop box and watching the page come back
cropped is what the control is for. A panel over the page would hide the one
thing worth looking at.

WHAT IS NOW REACHABLE

  Pages   which pages to keep or drop, turn them a quarter / a half / three
          quarters, reverse the order, move the page on the screen, crop to a
          box, insert a blank page, split into files of n pages
  Sheet   n pages to a sheet, fold into a booklet, add another file after this
          one, lay another file over it
  Marks   write a watermark across every page, in words somebody chose
  File    strip what runs rather than shows

Two up, Watermark and Sanitize leave the strip for the group each belongs to.
Two up becomes a number, so it is also three up and sixteen up.

THREE THINGS THAT WERE DEAD AND ARE NOT ANY MORE

None of this could have worked without them, and none of them was visible
before there was a panel to notice it in.

  A press arrived at a widget in SURFACE coordinates, and a container reads the
  point it is given as local to itself before adding its own origin back on. On
  a strip eight pixels from the top that is a near miss the sweep in the test
  papered over. On a panel that starts forty-six pixels down it is a press on
  the wrong row.

  The view got no events at all: handleClick reached only the toolbar. The Fill
  in panel, which is a panel of real widgets bound through observables, could
  not be pressed in a browser — it was a picture of controls.

  Char() returned false, always. Nothing could be typed anywhere, so 76.3% of
  the fields in the form panel — the text ones — had no way in.

The scroll view underneath was the fourth: toolkit v0.250.0 did not forward a
press to what is inside one, so every control in a scrolling panel was dead to
the mouse. That is fixed upstream in v0.267.0, which is why this bumps the
toolkit to v0.272.0.

A gap that is not fixed: neither ScrollView nor FormField implements the
unexported focusableChildren, so the toolkit's own focus walk stops at the
scroll view and cannot find a box inside it. The workbench therefore hands a
keystroke straight to the box it knows was last pressed. It works and it is
honest, but Tab does not walk the panel.

WHAT WAS MEASURED

Eleven verbs over 400 real forms taken evenly from the eleven issuing bodies
in /Users/Shared/pdfforms — 400 of the 1 633 real documents, the 635 vendor
fixtures left out. 4 400 outputs: every one written by ops, read back by our
own reader, parsed by pdfinfo and drawn by pdftoppm with ink on the page.

  4 311 of 4 311 applicable outputs passed all four
     89 not applicable: one-page documents where deleting page one leaves a
        document with none, which the panel refuses for that reason

Two apparent failures in the first run were the measurement's own and are
worth recording: pdftoppm pads the page number to the width of the page count,
so page-1.png is not always what it writes; and a blank page inserted at the
front is MEANT to come out blank, so the ink has to be looked for on page two.
Read the output, not the exit code.

In a real browser, with nothing eyeballed: the check now presses along the
strip from the right until the right-hand band of the canvas lights up, presses
down that panel until one of its verbs changes the band where the page is,
saves, and parses what came back to find (DRAFT) Tj in it. A panel that is
painted and gets no events looks exactly like one that works.

100% of statements, and every control in every group is reached by pressing it
where it was drawn rather than by calling its handler.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two boxes in one panel and every letter typed into the second went into the
first. A press on a box focuses it; nothing ever defocuses the one that had the
caret before, so both are focused at once and the workbench hands the keystroke
to whichever it finds first.

The toolkit does have a rule for this — a click walks the container for its
focusable descendants and focuses exactly the one it hit — but the walk needs
each container to enumerate its focusable children, and neither ScrollView nor
FormField does. Every control in this panel is inside both, so the walk finds
nothing and defocuses nothing.

So a press clears the caret from every box before it is routed, which leaves it
in exactly the box the press landed in, and in none at all when it landed
somewhere else.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit b43095c into main Aug 27, 2026
2 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.

1 participant