Skip to content

feat: polish the drawing UI and add a fading laser pointer - #394

Merged
devmobasa merged 38 commits into
mainfrom
feat/ux-polish
Sep 26, 2026
Merged

devmobasa merged 38 commits into
mainfrom
feat/ux-polish

Conversation

@devmobasa

Copy link
Copy Markdown
Owner

Make drawing controls easier to understand, reduce overlay clutter, and guide first-time users through drawing before offering background setup.

  • Add a configurable laser pointer with glowing, fading ink that stays out of saved sessions, undo history, captures, and exports.
  • Add the Pen feel panel with named levels and a live smoothing preview, plus inline meter and stepper options. Fix adjacent control click routing and fractional/multi-notch scrolling.
  • Replace arrow-style cycling with a menu of drawn previews. Show marker opacity through a swatch and tinted track that match the actual stroke transparency.
  • Improve toolbar clarity with contextual controls, preset previews, readable dark swatches, a stable layout menu, an Exit button, a larger restore tab, and capture visible by default.
  • Fully hide the idle toolbar and reveal it near the pointer. Hide toolbars behind modal overlays and prevent onboarding-card clicks from reaching the canvas.
  • Rework onboarding around first use, improve help readability and the canvas context menu, and stop preselecting Exit in the command palette.
  • Add Shape Pen recognition feedback, clearer capture notifications with Open folder and Copy path actions, and a launch notice for restored annotations. Reduce status-bar noise.
  • Fix selection repainting and keep newly placed text and sticky notes inside the output.
  • Update configuration, configurator controls, and documentation. Split the GTK top-bar tests into twelve focused modules while preserving all 19 test entry points and GTK isolation.

Pen smoothing and Shape Pen sensitivity were bare "− 3 +" steppers; with
Shape Pen two identical ones sat side by side and the value text was dim.
The shared model now gives the tool steppers a short caption ("Smooth",
"Detect"), and both toolbars draw it before the − half in the caption
tone. The readout between the halves uses the primary foreground at the
numeral buttons' weight, centered in its slot. The builtin tree reserves
the caption slot, so the width planner budgets it for both frontends.
Docked selection steppers keep their unit-bearing readouts uncaptioned.
With context_aware_ui on (the default), show_text_controls = true pinned
the text size, Bold, and font family onto every tool, so Pen, Shape Pen,
Marker, and Arrow carried three controls that changed nothing they draw.
Context-aware UI now offers them only where they reach the next shape:
text and sticky notes, arrows while auto-number labels are on, and step
markers, whose number uses the font's family and weight but takes its
size from the marker's own slider (so they get Bold and family, not the
text size). Both toolbars read the same spec. show_text_controls keeps
its meaning in classic mode (context_aware_ui = false); the example
config, CONFIG.md, the Settings tooltip, and the configurator row say so.
The Shapes picker read as active whenever any shape-family tool was up,
so Shape Pen, Line, and Arrow lit both their own strip button and the
picker, although full layouts leave those three out of the picker. The
picker now stands in for the active tool only while that tool lives
inside it (Rect, Ellipse, Blur, Spotlight, polygons; plus Shape Pen,
Line, and Arrow in the simple layout), or while its popover is open.
Both toolbars read the shared control state, so both change together.
The palette's black (#241F31) sits at about 1.2:1 against the dark
toolbar, so its swatch read as a gap in the color row, the radial ring,
and the help overlay's K badge. A shared helper in ui::theme::swatch
measures WCAG contrast (linearized sRGB) between a swatch, as it shows
over its chrome, and that chrome; below 3:1 (WCAG 1.4.11) the swatch's
edge becomes a 1.5px ring in the tone that contrasts with the chrome
(light on dark, dark on light). The GTK and builtin toolbar swatches,
the radial color ring, and the help overlay color badges use it; every
other swatch keeps its quiet hairline.
Filled slots already drew their tool glyph and color, but replaced the
slot number, and empty slots looked like five ordinary buttons. A filled
slot now keeps its number as a small caption in the corner opposite its
color swatch, and its tooltip and accessible name summarize what it
applies ("Preset 1: Pen, Red, 4px"; the color in the quick-color
palette's words, else hex; a user-given name leads). An empty slot shows
its number dimmed until hovered, and its tooltip says it is empty and
names the configured save binding ("click or press Shift+1 to save the
current tool"). Both toolbars change together; slot sizes are unchanged,
so the strip width planner needs no new budget.
Toolbar islands, popovers, and tooltips (built-in and GTK), the
context menu and submenus, board picker, properties panel, command
palette, modal cards, the capture chooser, the region-capture readout
and legend, toasts, and the help panel used 0.85-0.98 alpha fills, so
terminal text bled through labels and a white board turned the chrome
muddy. Their backgrounds are now opaque; theme colors, borders, and the
translucent full-screen dim scrims are unchanged.

The help panel's default ui.help_overlay_style.bg_color alpha moves
from 0.92 to 1.0 (example config and CONFIG.md updated); a user-set
alpha is still honored.
With ui.toolbar.idle_fade on, the top strip and its style pill used to
dim to 55% opacity, which read as a broken ghost bar over busy content.
They now fade out to fully transparent after ~4 s without drawing or
toolbar use, and fade back in when the pointer comes within 64 px of
the strip or touches the top screen edge, while the pointer or keyboard
focus is on it, and for 1.5 s after a tool or color change made away
from the strip (a modifier held for a shortcut does not count). A new
stroke keeps a visible strip up but never brings back a hidden one, and
the strip lingers for the idle delay after the pointer leaves. Open
menus still hold it; idle_fade = false keeps it always visible.

The hidden strip stays mapped at its size but takes no input: the
built-in surface gets an empty input region, the inline strip ignores
hits, and the GTK window disables input and shows its transparent
capture frame. Proximity is measured on the canvas surface.

Labels read "Hide toolbar when idle" in the settings popover and the
configurator; config docs, example config, and search terms follow.
Presenters had no disappearing ink. The new Laser tool (L, the toolbar
next to Marker in Regular and Advanced, or Laser Pointer Tool in the
palette) draws a glowing stroke with the same live preview path as the
pen. On release the stroke goes to a LaserInk owner instead of the
frame, so it never reaches undo, the session, selection, exports, or
captures, which render without transients.

Ink stays for hold_ms after the latest release, then fades over
fade_ms. Strokes drawn before the fade ends keep the whole group on
screen, so a multi-stroke gesture disappears together. Only a fade in
motion ticks the animation clock; a hold schedules one wakeup, idle
ink costs nothing, and damage follows each stroke's own path. Reduced
motion removes the ink in one step instead of fading.

Color, width, and timing come from a new [laser] section with
clamping, also on the configurator's Drawing page. Clear Canvas wipes
laser ink, locked presenter mode allows the laser, and light mode keeps
it when it is the active tool.
Click-select, rubber-band select, deselect, undo and paste changed the
selection without damaging where its halo and handles were or now
belong, so only the status bar repainted: new chrome appeared late and
old halos stayed on screen until an unrelated full repaint.

Route every membership change through one InputState helper that
measures the selection chrome (halos, handle frame, and a lone text
shape's resize handle) before and after the change and damages both.
Undo/redo and delete measure the chrome before they mutate the frame,
and releasing a held selection repaints the handles it hid. Measurer
aware _with variants replace the ad hoc damage at the old call sites.
A sticky note or text block placed near an edge, or one that grew past
the edge while typing, rendered partly off-screen and was committed
there, cutting the note off at the output boundary.

Every draft change already passes through the text preview damage
update, so fit the draft there: shift its anchor left or up until the
rendered block (glyphs, background, or note card and shadow) sits inside
the visible canvas rect, which follows pan and zoom. Commit fits once
more to cover style changes made since the last keystroke. An edited
block may keep reaching as far past the edge as it already did, so
opening a note that straddles the edge does not move it.
A modifier change that arrived without a key press, such as a
compositor wl_keyboard.modifiers sync or a Ctrl release, updated the
modifier state and the active drag tool but requested no redraw, so the
status bar kept showing the Ctrl drag tool (Rectangle) until the next
unrelated event.

Route modifier presses, releases, resets, and compositor syncs through
one update helper that requests a redraw when any modifier changed.
sync_modifiers now reports whether it changed anything. The canvas is
not dirtied; the render repaints only the status bar and tool preview
through their existing UI effect damage.
When Shape Pen turned a stroke into a shape, nothing said so or pointed
to the one-undo escape hatch, so the ink-back contract was easy to miss.

A recognized stroke now shows a small chip beside the new shape, such
as "Circle · Ctrl+Z keeps ink", using the configured undo shortcut (or
"Undo keeps ink" when undo is unbound). It holds for about a second,
fades out by 1.5 s, and stays opaque under reduced motion. It is overlay
chrome with its own UI effect slot, so each frame damages only its
footprint; it is never drawn into frames, exports, captures, or
sessions. Undo, redo, and board or page changes take it away.

[drawing] shape_recognition_feedback (default true) turns it off, also
on the configurator's Drawing page as "Name recognized Shape Pen
shapes".
Help, quick reference, the command palette, and the board, font, and
color pickers are drawn on the overlay surface, so the separately
stacked GTK and layer-shell toolbars covered them. A derived
suppression now unmaps both frontends while one of these modals is
open, alongside the existing capture-picker suppression. Toolbar
preferences are never written, so position, pin, minimize, and open
popovers come back when the modal closes. The inline built-in bars
now paint before popups so menus and panels draw above them.
The first-run card is painted over the canvas but every press on it
fell through to the active tool: clicking the card drew a dot, enabled
Undo, and ticked off the "Draw a stroke" step. The renderer now
returns the card's rectangle, and pointer, touch, and stylus presses
inside it are owned by the card through the chrome-press path. Other
buttons are swallowed with their release, idle hover over the card no
longer reaches the canvas, and the cursor shows the default arrow.
The first-run tour opened on a keyboard-only background-service prompt
before the user had drawn anything. It now runs value first: draw and
undo, the toolbar and how to leave (Esc), color and thickness, quick
access, and finding commands, with background mode offered last. The
rebind jargon left the find step; the replayable tour still covers it.

Card actions are real buttons (Got it, Set up, Not now, Skip tour) with
their keys shown beside them; Enter, Y/N, and Shift+Esc still work and
no longer fire mid-gesture or with Ctrl/Alt held. Buttons run when press
and release land on the same button and highlight on hover.

onboarding.toml moves to v7: a pre-v7 tour still waiting on the old
first step restarts at draw and undo, the retired wait-draw step maps
forward, and completed or skipped profiles are never re-enrolled.
With nothing typed the palette listed the registry in order, so Exit
opened highlighted at the top and Ctrl+K, Enter closed the overlay,
with Clear Canvas right below. The empty query now lists recent
commands, then everyday ones (Undo, Redo, pen, marker, eraser, text,
select, region capture, whiteboard, help), then the rest by category,
and a trailing "Clear, delete & exit" group. Exit and destructive
commands go there even when recently used, so none is preselected.

The edit/unbind/reset icons now draw only on the selected or hovered
row, where the trash icon no longer reads as "delete" beside Clear
Canvas. Their space stays reserved so hovering never shifts text, and
Ctrl+E, Ctrl+Delete, and Ctrl+R still act on the selected row.
The canvas menu put Clear All second, right under Paste, offered Paste
with nothing to paste, had no Undo, Redo, capture, or way out, and drew
its key hints in a tiny strip below the box.

It now reads in groups: Undo and Redo (dimmed without history), Paste
and Capture Region..., view and boards, the other surfaces, Clear All,
and Exit last (Hide Overlay when the daemon owns the overlay). Paste is
enabled once shapes were copied or a capture went to the clipboard;
the system clipboard cannot be probed without blocking, so that is all
it knows. The highlight row uses the action's own short label,
Highlight, as the toolbar does. The shape menu gains the same dividers
and closing Exit row.

Dividers sit on row edges, so rows keep one pitch for hit-testing and
keyboard focus. The key-hint footer is inside the box at 12px, and the
box is sized for every hint it can show.
Rows now read "action label ... key chips": labels form the left
column and keys start one gap past the widest label, joined by a
subtle dotted leader, so the eye can follow each row.

Actions without a binding are hidden by default. A new footer pill,
"Show Unbound" (or Tab, since printable keys type into search),
brings them back with a muted "Not bound" marker instead of a key
chip. Search still finds unbound actions. The choice lasts for the
process.

Body text now renders at the configured help font size instead of
80% of it, footer pills use the body size, and secondary text has a
12px floor (11px in key chips). Sizes are logical pixels, so they
follow the output scale. Page 1 still fits a 1080p output.

Also fix the search-mode nav line, whose segments advanced by ink
width and overlapped ("Esc: Close" / "Backspace: Remove").
Add shared readable-floor tokens to the overlay theme: 12px for
labels, hints, and status lines, 11px for keycap labels, and 13px for
control labels and hint strips. Overlay chrome is laid out in logical
pixels, so the floors follow the output scale.

Apply them where text was too small:
- capture action panel: labels 11 -> 13px, key chips 8.5 -> 11px,
  toggle 10.5 -> 13px, status 10.5 -> 12px; the rows and controls
  grow so nothing clips
- region-capture hint strip: 12 -> 13px, drawn through the same text
  layout that measures it, in a taller strip
- radial menu: shortcut and thickness keycaps 9 -> 11px, sub-ring
  labels 11 -> 12px
- help overlay: its floors now come from the shared tokens

The context-menu footer is left to the context-menu rework.
The capture toast printed the full absolute path and a "(Ctrl+Alt+O
opens folder)" note across the whole screen. It now reads, for
example, "Saved screenshot_2026-09-25_213231.png · Copied to
clipboard". A path without a usable file name falls back to a
~-relative path.

A saved file adds two buttons, reusing the toast's action chips:
"Open folder · Ctrl+Alt+O" runs the existing Open Capture Folder
action (its label shows the live binding), and "Copy path" copies the
full path through the text clipboard pipeline and confirms with
"Path copied". The toast stays up for 8 s so the buttons are
reachable. Copy-only captures say "Copied to clipboard", and a saved
clipboard-fallback file uses the same toast. Error toasts keep their
full detail.
With a single output, "Output: WAYLAND-1" told the user nothing, and
the permanent "About v0.9.26" chip took space in every frame.

The output item now shows only while two or more outputs are
connected. The backend reports the connected-output count along with
the label (excluding an output that is being destroyed). An explicit
active_output_badge = false still hides it, and the new
ui.active_output_badge_always keeps it with a single output. A plain
active_output_badge = true, which the example config has always
carried, is not treated as a request to show a single output's name.

The About/version chip is now off by default (show_status_about =
false). It stays available as an opt-in item, and About remains in the
toolbar and the help overlay.

Update config.example.toml, docs/CONFIG.md, README, and the
configurator's status-bar contents page and search terms.
Relaunching restored earlier ink onto the transparent overlay board,
where it sits over whatever is on screen now, with no notice.

After the launch-time session load, when the active board is
transparent and its page has at least one shape, show "Restored 7
annotations from last session" with a Clear button. Clear runs Clear
Canvas, so it is undoable. The toast uses the existing action-chip
support and stays up for 8 s.

Stay quiet for overlays the daemon reopens on a toggle (they carry
the overlay-child generation variable and show what the user just
had on screen), for loads without board data, for solid boards, and
for later output or named-session switches.
With a GTK toolbar popover open, the toolbar surface keeps keyboard
focus on its own Wayland connection, so Escape did not close the
popover and tool shortcuts typed meanwhile were dropped.

The GTK window and each popover now relay key presses to the backend,
which routes them through the same press path as the overlay's own
keyboard (with GTK's modifier state for that press only). Modifier
presses, Tab, and keys owned by a focused slider or entry stay in GTK.

Key routing now closes any open top-strip menu (shapes, overflow,
Canvas/Session/Settings) on a key press: Escape only closes it, any
other key closes it and still reaches its binding. The built-in
frontend shares the same routing.
The chrome island's layout button cycled Simple -> Regular -> Advanced.
Each preset gives the strip a different width, so the button moved out
from under the pointer and a second click landed on the canvas. Regular
and Advanced also rendered the same strip.

The button now opens a small menu (a new LayoutMenu top-strip flyout)
listing the three presets with a one-line description each and the
current one marked; choosing a row applies the preset and closes the
menu. Both frontends render the rows from one shared model, and the
menu closes like the other flyouts (Escape, click-away, tool picks).

Advanced now puts Rectangle, Ellipse, Blur, and Spotlight on the strip
and keeps only the polygons in the Shapes picker; under width pressure
those tools move to the overflow menu before Arrow and Line. Layout
changes still persist through runtime-ui.toml as before.
A mouse-only user had no way to close the overlay: the chrome island
offered Layout, About, Pin, and Minimize, and Minimize turned red on
hover, which read as "close".

A new Exit entry (item id top.chrome.exit) ends the chrome island in
both frontends. It runs the Exit action as an explicit exit request, and
its tooltip names the configured Exit keys. When the daemon spawned the
overlay it reads "Hide overlay", since the daemon keeps running. It is a
typed toolbar item, so customization can hide it.

Minimize keeps its round chrome button but now lifts to a neutral tone
on hover; red stays reserved for destructive actions.
Capture was reachable only through shortcuts and the command palette:
the toolbar screenshot item shipped in the default hidden list.

The default hidden list is now empty, so the capture button shows by
default. It moves from the annotation utilities into the history
island, after Undo/Redo and before the overflow toggle, and it starts
the interactive region capture (select, then Copy / Save / Both /
Board) instead of the selection-with-defaults capture. It stays a
typed, hideable item and still yields to the overflow menu first
under width pressure. Configs that author their own hidden list keep
it; the example config, docs, and README now describe the new default.
After Minimize the strip collapsed to a 64x24, low-contrast tab that sat
exactly over the restored strip's Select button, so a double-click
restored the strip and then selected a tool.

The tab is now 104x32 in both frontends (one shared size), with a
raised, outlined body, the restore glyph, and a "Tools" caption; its
tooltip and accessible label still say "Show toolbar". Restoring from
the tab or the micro chip arms a short guard (about a double-click
interval) in the input toolbar state, and the shared toolbar dispatch
drops clicks while it runs, so the second click cannot reach the
controls that reappear under the pointer.
The Settings popover greeted every visit with "Runtime preferences are
saved separately" and the raw runtime-ui.toml path.

Healthy runtime persistence now reads "Toolbar changes are saved
automatically". The storage path moved into a collapsed "Details"
disclosure: settings notices carry a detail flag, the shared settings
model offers a Details / Hide details toggle, and a new
SetSettingsDetailsOpen event keeps its state in the input toolbar
state, so both frontends render the same rows. Warnings, recovery
files, and the reset action stay in view; the reset button now reads
"Reset toolbar changes".
Add a Pen feel panel with named levels and a live smoothing preview,
plus inline meter and stepper options in both toolbar frontends.
Expose the presentation setting in the configurator and config guide.

Prefer visible controls over inflated neighboring hit targets. Accumulate
fractional wheel travel per meter, clear it on pointer departure, and apply
GTK wheel deltas to live application state so delayed snapshots cannot
lose rapid or coalesced notches. Cover hit routing and GTK scroll callbacks.
A bare "90%" beside the marker opacity slider did not say which way was
see-through, and the tooltip described it backwards. The track now fades
from clear to solid in the current color, and the readout is a swatch of
a marker stroke over sample text at the current opacity. Both use the
alpha a marker stroke actually gets, so a translucent color previews as
translucent as it draws. The percentage moves into the tooltip, which now
reads "Marker opacity: 90% solid".
The arrow style button cycled Standard, Pointy, Curved, and Double on each
click, hiding three styles behind names that do not say what an arrow looks
like. The style pill now carries a chip showing the next arrow's style drawn
and named, and clicking it opens a menu listing every style as a preview
drawn from the arrow outline the canvas fills. Choosing a style sets it and
closes the menu; the keyboard action still cycles.

The menu is a top-strip menu like the layout menu in both frontends: it
closes the other menus, hangs from the chip, and joins the built-in
surface's input region.
@devmobasa
devmobasa merged commit e849384 into main Sep 26, 2026
3 checks passed
@devmobasa
devmobasa deleted the feat/ux-polish branch September 27, 2026 10:15
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