Skip to content

Show what a tap would do while the pen hovers, and declare 1.0.0 - #62

Merged
marcosqlbi merged 6 commits into
mainfrom
feature/pointer-feedback
Aug 23, 2026
Merged

Show what a tap would do while the pen hovers, and declare 1.0.0#62
marcosqlbi merged 6 commits into
mainfrom
feature/pointer-feedback

Conversation

@marcosqlbi

Copy link
Copy Markdown
Collaborator

A hovering pen used to say very little about what would happen if it touched down.
The laser showed the same flat dot as every other tool, and a reversed pen showed
nothing at all. Both now show the tool that would actually act, which is the change
most of this branch is about. Two input bugs surfaced underneath while testing it,
and the version moves to 1.0.0.

The pointer shows what a tap would do

With the laser selected, hovering draws the laser itself — a red core inside a soft
halo — trailing a short comet as it moves. The comet is every sample from a 110 ms
window, so its length is the distance the pen covered in that time: sweep fast and it
stretches, stop and it collapses to the dot. That mirrors Microsoft Whiteboard, which
is where the request came from.

Hover state is kept apart from contact state deliberately. A lifted pen leaves its
stroke open for a moment so a resumed line joins the previous one, and that window is
measured from the contact head being gone — a hovering pen must not fill it.

With the eraser, whether from the toolbar or by reversing the pen, hovering draws a
dashed square around exactly what a tap would clear. Erasing removes a patch of board
rather than a point, so a dot would have said nothing useful about reach. The square
is sized from the same number EraseAt uses, so it cannot outline an area the eraser
will not clear, and that reach is in screen pixels, so it holds its size at any zoom.

Trail weight is a setting

A pen reports very little pressure on a quick tap — 0.06 is typical — which drew a
1.7 px line at half opacity: thin and faint enough to miss on a projector. Three
weights raise that floor without changing what a firm stroke looks like. Light is the
default and matches the previous behaviour exactly.

The options are drawn rather than named, side by side, each showing a light tap above
a firm stroke. The firm stroke is identical across all three, so what the setting does
and does not change is visible without reading anything. The samples are computed from
the same floors the trail itself uses, so a preview cannot promise something the laser
will not deliver.

Two input bugs

Clicking the pen's barrel button over a hovering pen drew a short laser trail without
the pen ever reaching the glass, and the first real tap afterwards drew nothing.

The tablet driver synthesises a full stylus down for the button press. It claims
everything a real touch claims — not in air, tip switch pressed — so only the pressure
gives it away, because nothing is pressing on the tip. That phantom set the contact
flag, and the barrel handler started a trail on the strength of it. It is now rejected
in the window and in the ink renderer, which runs on the raw input thread and never saw
the window's guard.

Two more problems sat underneath. The tracker feeding hover positions kept a contact
flag of its own, and the phantom's matching up does not arrive until after the next
real touch, so every hover in between was dropped. And this tablet reports InAir
false while hovering, so three places reading InAir as "hovering" fought that
tracker on every sample. Both now defer to the window's contact flag, the only copy of
that state worth keeping.

Finally, WPF raises no stylus down at all for the first landing after a phantom, having
never closed the phantom's own down, so the laser recovers contact from tip pressure —
the one signal that stayed honest throughout. This is a workaround for driver behaviour,
not for anything the application does wrong, and there is no way to make WPF issue the
down it skipped.

Toolbar width

The tool palette was one button wider than it needed to be and overflowed the presenter
picture-in-picture that the default top-right placement exists to hide under. The dual
layout sizes its shared colour column from the pen's six swatches, and the highlighter
row carried three tool buttons against the pen row's two, so that row set the width of
the whole palette. The highlighter has four swatches, leaving two swatch widths of that
column unused; the select button now sits there, which brings the palette from 328 to
286 logical pixels.

1.0.0

decisions.md carried "whether 1.0.0 is declared, and on what" as an open question.
What 1.0 was waiting on was in the product rather than the pipeline — Preferences,
.wimport, the Explorer and VS Code previews, the documentation site, and Finger
drawing — and all of it shipped during 0.9.x. The question is now decision 20, and
TODO.md moves with it.

Delivery is unchanged: this merge publishes a pre-release, and promotion stays an
approval on that run.

Verification

Every commit builds independently in a clean worktree with zero warnings, and the Core
smoke tests pass. The smoke tests gained cases for the new setting, matching those
already covering the laser hold mode.

The pen behaviour was tested on hardware by @marcorusso — the tablet-specific bugs above
are not reproducible without it. The laser and eraser rendering was verified by driving
the real surfaces headlessly and rendering the result, rather than by eye.

🤖 Generated with Claude Code

marcosqlbi and others added 6 commits August 23, 2026 15:44
The palette was one tool button wider than it needed to be. The dual layout
sizes its shared colour column from the pen's six swatches, and the highlighter
row carried three tool buttons against the pen row's two, so that row set the
width of the whole palette and overflowed the picture-in-picture the default
top-right placement exists to hide under.

The highlighter has four swatches, leaving two swatch widths of that shared
column unused. The select button now sits there, docked to the column's right
edge, which drops the palette from 328 to 286 logical pixels without moving
anything else. A DockPanel rather than overlapping elements, so a palette that
ever gains colours squeezes the swatches instead of drawing over the button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clicking the barrel button over a hovering pen made a short laser trail appear
without the pen ever reaching the glass, and the first real tap afterwards drew
nothing.

The tablet driver synthesises a full stylus down for the button press. It claims
everything a real touch claims - not in air, tip switch pressed - so only the
pressure gives it away, and nothing is pressing on the tip. That phantom set the
contact flag, and the barrel handler then started a trail on the strength of it.

The phantom is now rejected in the window and in the ink renderer, which runs on
the raw input thread and so never saw the window's guard; left alone it opened a
wet stroke that was torn down mid-flight when the tool switched.

Two further problems surfaced underneath. The tracker feeding hover positions
kept a contact flag of its own, and the phantom's matching up does not arrive
until after the next real touch, so every hover in between was dropped. And this
tablet reports InAir false while hovering, so three places that read InAir as
"hovering" fought the tracker on every sample. Both now defer to the window's
contact flag, which is the only copy of that state worth keeping.

Finally, WPF raises no stylus down at all for the first landing after a phantom,
having never closed the phantom's own down. The laser recovers contact from tip
pressure instead, which is the one signal that stayed honest throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hovering with the laser showed the same flat red dot every tool uses. Microsoft
Whiteboard shows the laser itself - a red core inside a soft halo, trailing a
short comet as it moves - which reads as a pointer rather than as a cursor.

The trail surface now takes hover samples as well as contact ones and keeps
every sample from a 110ms window, so the comet is the distance the pen covered
in that time: sweep fast and it stretches, stop and it collapses to the dot. A
170px cap keeps a flick from streaking across the board. Hover state is held
apart from contact state on purpose: a lifted pen leaves its stroke open for a
moment so a resumed line joins the last one, and that window is measured from
the contact head being gone, which a hovering pen must not fill.

The head is drawn once for both, so the instrument does not change appearance
the instant it touches glass. Its centre is now solid red rather than near
white, which is what the reference shows and what survives a projector.

This also introduces the trail weight the surface reads. A pen reports very
little pressure on a quick tap, thin and faint enough to miss in a recording,
and the weights raise that floor without touching what a firm stroke looks like.
The numbers live beside the enum in Core so the renderer and anything previewing
it cannot drift apart. Preferences follows separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trail weight is a setting about how something looks, so a dropdown of three
words made the reader imagine the result. The three options are drawn instead,
side by side, each showing a light tap above a firm stroke: the firm stroke is
identical across all three, so what the setting does and does not change is
visible without reading anything.

The samples are computed from the same floors the trail itself uses rather than
being drawn to look about right, so a preview cannot promise something the
laser will not deliver.

Light stays the default, matching the behaviour before the weights existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reversing the pen erases on contact, but hovering with it showed nothing at all
- the hover dot was suppressed for a reversed pen, so the one pose that most
needs to explain itself explained the least. A dot would not help much either,
since erasing clears a patch of board rather than a point.

Hovering with the eraser now draws a dashed square around exactly what a tap
would remove, as Microsoft Whiteboard does. It is sized from the same reach
EraseAt uses, so it cannot outline an area the eraser will not clear, and that
reach is a fixed number of screen pixels, so the square holds its size at any
zoom.

The eraser also takes precedence over the laser while hovering: what a tap
would do is the question the pointer answers, and a reversed pen erases whatever
tool is selected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What 1.0 was waiting on was in the product, not the pipeline: Preferences,
.wimport, the Explorer and VS Code previews, the documentation site, and Finger
drawing. All of them shipped during 0.9.x and nothing numbered was left behind
them, so the open question of what had to be true before the version stopped
being 0.x has an answer.

VersionPrefix is the only place the desktop version is written; the assembly
stamp, the informational version, the installer file names, and the Store
identity version all derive from it. decisions.md records the call and no longer
carries it as open, and TODO.md's statement of where the project stands moves
with it.

Delivery is unchanged. A merge publishes a pre-release and promotion stays an
approval on that run, which is what let the number describe the product rather
than an event in the pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@marcosqlbi
marcosqlbi merged commit 9d2060b into main Aug 23, 2026
4 checks passed
@marcosqlbi
marcosqlbi deleted the feature/pointer-feedback branch August 23, 2026 14:04
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