Repository navigation
Fix ten editor issues: selections, clipboard, colour picker and filters - #13
Conversation
paintAt drew straight onto the layer canvas, so a stroke crossing a marquee, lasso or wand boundary painted over the whole layer. Selections already carry a geometric path, so clipping uses a canvas clip region built from it rather than compositing through the raster mask - that stays correct for the eraser, which draws with destination-out. A selection that encloses nothing now clips everything away instead of painting freely. selectionGeometry is kept free of DOM types so it is unit testable; jsdom has no Path2D. Adds site/scripts/verify-editor.mjs, which drives the real editor in a browser and samples the canvas to prove paint lands inside the selection and nowhere else. Fixes #3
The lasso cursor was a rope glyph with its hotspot at 3 3, so the point it actually selected from sat outside the drawn shape. Replaced with a centred crosshair carrying a small lasso mark, hotspot on the crosshair, drawn with a dark halo under a light stroke so it stays visible on both light and dark photos - matching how the wand and eyedropper cursors are built. Fixes #4
Ctrl+D always duplicated the layer, which is rarely what you want while a selection is active; it now deselects when there is a selection and duplicates otherwise. Ctrl+J duplicates unconditionally, and the menus name the new bindings. The eyedropper also now hands back whichever tool was active before it, so sampling a colour mid-stroke no longer costs a trip to the toolbar. Fixes #6 Fixes #7
Alt+right-drag pinned the OS cursor with SetCursorPos after every move, which fired a synthetic move of its own; the ignoreNext flag meant to swallow those dropped real events too, so the size lurched. Pointer lock gives relative deltas without moving the cursor at all, and releasing it puts the cursor back where the drag began - the behaviour that was being approximated. pin_cursor stays as the fallback when a lock is refused. The new brush-resize check asserts the size climbs without reversing. It cannot reproduce the original jitter, which needed the native SetCursorPos path, so the desktop build still wants a manual pass. Fixes #5
Dragging inside a selection with Cursor (A) now floats and moves the selected pixels; Alt+drag moves only the outline, which also carries the hit test and the paint clip with it. The selection hit test runs before the focus box so dragging inside a selection no longer flips to Move. Moving pixels was silently broken beyond the missing binding. The compositor caches one GPU texture per layer generation, and the floating selection mutated the layer canvas through touchPixels, which bumps only the global generation - so the pixels moved in memory while the screen kept showing the stale texture. extractFloatingSelection and stampFloatingSelection now invalidate the layer they mutate, the way the paint path already did. The drag preview also re-encoded a document-sized PNG through toDataURL on every pointer move; it now draws the floating canvas directly. Fixes #11
There was no way to choose a colour: the only route was sampling one with the eyedropper, and both swatches were inert. Adds a hue/saturation wheel with a value slider, plus RGB, HSL, alpha and hex fields that all drive each other. HSV is held as the source of truth while the dialog is open so dragging through black or grey does not throw away the hue. Both the options bar chip and the properties panel swatch now open it. Also corrects the options bar hint, which promised Ctrl+Alt+right-drag for brush size when the handler only wants Alt. Fixes #8
Both Edit entries were greyed out, so a selection could be made but never taken anywhere. Ctrl+C and Ctrl+X now render the selected region through the selection mask, cropped to its bounds rather than the whole document, and Cut clears it as one undoable step. The region is kept in process as well as written to the system clipboard, so pasting back inside Photovibe is exact instead of a PNG round-trip. The menu entries enable and disable with the selection. Fixes #9
Copying a file in Explorer puts a CF_HDROP path list on the clipboard rather than a bitmap, so readImage saw nothing and Ctrl+V did nothing. Adds a read_clipboard_files command that reads that list through the Win32 clipboard API, and Paste now falls back to loading the first image path when there is no bitmap. Dropping a file did nothing either. The desktop webview swallows the DOM drop event, so the app listens to Tauri onDragDropEvent for paths, with an HTML5 drop handler covering the browser build. Both show a drop target while a file is over the canvas. Fixes #10
The Image menu offered only size, and the Filter menu was entirely disabled. Adds Adjustments (brightness/contrast, exposure, hue/saturation, levels, invert, grayscale, sepia) and Filters (gaussian blur, sharpen, vignette, add noise), every one of which respects an active marquee or lasso and falls back to the whole layer otherwise. The dialog previews on the canvas itself by running the filter against a snapshot of the affected region, so Cancel restores exactly and Apply opens the history step on the unfiltered pixels rather than on a preview. The maths works on a structural pixel buffer rather than ImageData, so it is unit tested directly - jsdom has no ImageData. Blur is separable and weights colour by alpha so transparent pixels do not bleed dark edges inward. MenuBar grew submenu support along the way. Two bugs fell out of that: the chevron was part of the accessible name, and clicking a submenu already opened by hover toggled it shut. Modal now carries a dialog role. Fixes #12
The selections page claimed painting was already clipped to the selection; it was not until this round, and the page now also covers moving the chunk versus the outline, cut and copy, and filters. Shortcuts corrected: duplicate layer moved to Ctrl+J, Ctrl+D deselects, brush resize is Alt+right-drag rather than Ctrl+Alt, and the eyedropper returns to the previous tool. Adds the colour picker and drag-and-drop. The roadmap and the landing page lists move adjustments, filters and cut/copy out of what is coming and into what works. Screenshots re-captured for the new menus and options bar.
Paste from File Explorer never ran: handlePaste checked the in-process copy first and returned unconditionally, so a single Ctrl+C inside the app shadowed every later paste from outside it. The system clipboard is now consulted first - our own Copy writes a PNG there too, and PNG round trips losslessly, so nothing is given up. The Win32 CF_HDROP reader was correct all along; src-tauri/examples/clipboard_files.rs exercises it without launching the app. Only the first undo reached the screen. replaceAll cleared the layer generations, so every restore reset them to 1 and the compositor, which caches one texture per layer generation, skipped the upload from the second restore onwards. Generations are now monotonic. Redo also took its label from the caller's placeholder, so the menu read Redo Current. Shortcuts died after touching a slider: the key handler treated every INPUT as text entry, and a range input keeps focus after a drag. Only real text entry swallows shortcuts now. The filter dialog covered and dimmed the canvas it was previewing on. It now opens top-left and dims the interface around the canvas with four panels, leaving the image itself at full brightness. Eraser moved above the eyedropper in the tool column. Fixes #16 Fixes #17 Fixes #18 Fixes #19
Second round — fixes from the first QA pass
Paste from File Explorer never ran (QA step 17). Not the Win32 code — that was correct. Only the first undo reached the screen (#16). Shortcuts died after touching a slider (#17). The key handler treated every The filter dialog covered and dimmed the canvas it previews on (#18). It opens top-left now, and the dim is drawn as four panels around the canvas, so the image keeps its true colours while the interface recedes. Eraser moved above the eyedropper (#19). Shortcuts unchanged. Two new checks cover the undo regression: |
Fixes #3, Fixes #4, Fixes #5, Fixes #6, Fixes #7, Fixes #8, Fixes #9, Fixes #10, Fixes #11, Fixes #12, Fixes #16, Fixes #17, Fixes #18, Fixes #19
Fourteen issues, one commit each, in two rounds — ten reported up front, four more found during the QA pass. Issues #1 and #2 are deliberately not here; they have their own branches (#15 and #14) so they stay independently reviewable.
The bugs that were deeper than they looked
Painting ignored selections entirely (#3).
paintAtdrew straight onto the layer canvas with no clipping at all. Selections already carry a geometric path, so clipping now builds aPath2Dand uses a canvas clip region rather than compositing through the raster mask — which matters because it stays correct for the eraser, and the eraser draws withdestination-out.Moving selected pixels was broken past the missing binding (#11). The compositor caches one GPU texture per layer generation, but the floating-selection code mutated the layer canvas through
touchPixels, which bumps only the global generation. Pixels moved in memory while the screen showed a stale texture.Only the first undo ever reached the screen (#16). The same cache, a different cause:
pixelStore.replaceAllcleared the layer generations, so every restore reset them to1and from the second restore onward the generation matched what was already cached, so the upload was skipped. Generations are monotonic now.Shortcuts died after touching a slider (#17). The key handler treated every
<input>as text entry, and a range input keeps focus after a drag — so adjusting the brush size silently disabled Ctrl+Z until you clicked elsewhere. This is most of why undo felt erratic.Paste from File Explorer never ran (#10, then again in QA). The Win32
CF_HDROPreader was correct throughout —src-tauri/examples/clipboard_files.rsexercises it without launching the app. The fault was thathandlePastechecked the in-process copy first and returned unconditionally, so one in-app Ctrl+C shadowed every later paste from outside.Everything in the branch
Incidental fixes found on the way:
MenuBargained submenu support, and two bugs fell out of it — the chevron was part of the accessible name, and clicking a submenu already opened by hover toggled it shut.Modalgained adialogrole. The options bar claimed brush resize wasCtrl+Alt+right-dragwhen the handler only wantsAlt. The floating-selection drag preview re-encoded a document-sized PNG throughtoDataURLon every pointer move. The Edit menu read Redo Current instead of Redo Brush.Verification
ImageDataso it is testable directly; jsdom has neitherImageDatanorPath2D.site/scripts/verify-editor.mjs(pnpm verifyfromsite/) drive the real editor in a browser and sample the canvas: paint clipping in and out of a selection, brush resize monotonicity, moving pixels versus outline, cut/paste, filters scoped to a selection, filter preview and cancel, and undo of both a brush stroke and an erase.pnpm exec tsc --noEmitandcargo checkboth clean.Two things still need a human on the desktop build, because neither path exists in a browser: the Alt+right-drag resize (#5 — the original jitter came from the native
SetCursorPos) and File Explorer paste and drag-drop (#10 —CF_HDROPis Win32 only).Docs
site/src/content/docs/selections.mdclaimed painting was already clipped to the selection. It was not, until this branch. That page, the shortcut tables, the roadmap and the landing-page feature lists are corrected, and the screenshots are re-captured.