diff --git a/AGENTS.md b/AGENTS.md index d25f4ab..70cd78e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -173,6 +173,8 @@ Platform adaptations, each forced by a missing Qt analog: - Shift on a pixel drag and Select All from the list (13.10): `CanvasView::moveSelectionTool` (`EditorCanvas+Selection.cpp`) twins Swift's `60bde4f`: with Shift held a Ctrl-drag of selected pixels zeroes the shorter axis of the offset, lengths compared (`std::abs`), a tie keeping across, as `dragSelection` already does for the outline; read from each move's own modifiers, as Swift's `event.modifierFlags`. `451281e`: Swift's table now hands Select All to the canvas (`session.selectAll()`, guarded on a document) instead of selecting every row, so the twin's `NativeLayerList::selectAllRows` (12.5b's round-3 fix) and the menu's `focusedList` branch are gone: a focused list falls through to `session().selectAll()`, which guards on the document itself. A rename's editor is a `QLineEdit`, so it keeps its own Select All first, as Swift's field editor. Tests: `SelectionCanvasTests::shiftKeepsMovedPixelsOnALine` (across, down, a tie, a long left beating a short down, without Shift, the release keeping the last offset), `SelectMenuTests::selectAllInAFieldIsTheFields` (a real Ctrl+A on the focused list selects the canvas, history "Select All", layers and the active one unchanged, the list keeping focus; Swift's `selectAllInTheLayersPanelSelectsTheCanvas`). Mutants: 5 on the Shift branch, all caught (the `abs`-less comparison caught after the left-beats-down step was added). No survivors. - Why a brush cannot paint (13.11, `133c34a`): `EditorSession::paintRefusal` (`EditorSession+Brush.cpp`, declared in `EditorSession+Brush.h`) twins Swift's `paintRefusal`: none while `canEditLayers` refuses, without an active layer or when `canPaint` holds; then several layers, a folder's pixels, a hidden layer or one in a hidden folder, a mask turned off, an adjustment's pixels, an empty selection, in Swift's order and words, with Ctrl+D for ⌘D and the curly quotes kept. `beginBrush` splits Swift's guard: a tool that cannot paint the target (Spot Healing or Clone Stamp on a mask, a tool that is no brush) returns silently, a refused target sets `brushError` to the refusal (none when busy, so a waiting error is cleared, as Swift's `brushError = paintRefusal`). `beginWarp` sets the mask's words or the refusal. Fix beyond Swift: Swift's gradient sets the refusal only past its first guard, which is reached with a refused target only while a gradient is pending, where `canEditLayers` refuses and the refusal is always nil; so Swift's gradient says nothing on an empty selection, which its commit message says it explains. The twin's `beginGradient` sets the refusal at the first guard (no pending gradient, `canPaint` refusing); Swift's second assignment, always nil there, is left silent. `invertSelection` leaves no selection when the inverse is empty (Select All then Inverse), one `Inverse` step, as Swift's. Tests: `PaintRefusalTests` (every refusal and its order: two layers before a folder, a folder's mask turned off before the folder, an adjustment's mask before the adjustment, a layer inside a hidden folder; the gradient and the Smear; a busy session clearing, another tool silent), `SelectionTests::inverseOfEverythingDeselects` (Swift's). Equivalent mutants: `paintRefusal`'s `canPaint()` term (when `canPaint` holds every later test is false) and the empty-selection test without `isEmpty` (reached only when `canPaint` refuses for no listed reason, which needs an active layer outside `selectedLayerIDs`, which no session call makes). Painting at the layer's own resolution (13.11, `d3170ab`): `BrushStroke::Clone` is Swift's tuple `{image, placed, inGrid}`: the image painted through the tip and where it sits, in document pixels or in the stroke's own grid. `clonePixels` maps tile pixels through the grid (and `pixelToDocument` unless `inGrid`) into `placed`, scaled to the image, so a translation alone still copies or shifts by the fraction as before. `BrushStroke::gridRect` (in `BrushStroke+Commit.cpp`, beside `transform`, as Swift's) carries a document offset into the grid through the inverse map's linear part (`CGSize.applying`), turned, scaled and flipped as the layer. `cloneSample` takes the stroke and the offset: the stroke layer's own pixels placed at `gridRect(sourceRect, offset)`, or, sampling all layers, the composite at document size placed `offset` away; `blurSample` takes the stroke: the layer's pixels or mask drawn nearest at the margin of a region `ceil(3σ)` round `sourceRect`, σ the canvas's tenth of the brush (1.5 to 30) divided by the grid's document size per pixel (`sqrt|det|`) and capped at half the longer side, a mask on its edge tone first, then `PixelAdjust::gaussianBlur` (a mask's edge repeated, pixels clear past theirs) at σ × fit, where fit shrinks the sample to a budget of `min(maxSurfacePixels, max(16 M, four canvases))`; it returns the region as `placed`, `inGrid`. Swift's old mask sample (the placement's black box, `drawCoverage`) is gone with it. `beginBrush` makes the stroke first, then samples into it, as Swift's; a sample that fails returns without a stroke. The Smear's commit places its copy at document size. The brush and eraser: Swift's commit only changes its CPU fallback's stamp (`stampDiameter`), dead in the twin (8.1); the held judgment holds: the twin's coverage is rendered in the stroke's grid, so `NativeResolutionPaintTests` passes Swift's erase cases (scaled down, unequal, turned, and on the canvas scaled back up) and the blank layer's document resolution without any change to `BrushStroke::grid` or `BrushCoverage`. Tests: `NativeResolutionPaintTests` (Swift's seven, plus the offset carried through a flipped and a turned layer, pixel for pixel), `BlurToolTests` (`theSampleFollowsTheBrushSize`, `theSampleIsInTheLayersOwnPixels`: twice as fine, and a tiny layer's cap; `aHugeLayersSampleIsMadeCoarser`: 16 M and four canvases; `aMaskSampleKeepsItsEdgeTone`), the tests' document clones through `documentClone` (`BrushFixtures.h`). Cancel and Apply, and a scaled layer's row (13.11, `67cc31e`): `TransformInspector` puts Cancel and Apply in a row of their own (`pendingTransform`, spaced 12) that shows only while the edit waits (`transformEdit()->persistent`: typed values, Ctrl+T, a distortion begun so); a handle drag's edit leaves it unseen. Swift's `.opacity(0)` keeps the buttons in the layout and their keys alive, so the row keeps its place under a `QGraphicsOpacityEffect` faded over 0.12 s with CSS's ease-out (`QEasingCurve` Bézier 0, 0, 0.58, 1, SwiftUI's `.easeOut`), passes clicks through (`WA_TransparentForMouseEvents`, Swift's `allowsHitTesting(false)`) and takes no focus while unseen; Return and Escape still reach the edit through the bar, and a remapped key through `NativeShortcut` (the buttons stay `isVisible`). Known difference: Qt has no per-widget accessibility hiding short of hiding the widget, so a screen reader still finds the unseen buttons (Swift's `accessibilityHidden`). `sizeLabel(const ImageLayer &)` (declared in `NativeLayerList.h`, defined in `NativeLayerList+Cell.cpp`, Swift's extension in its UI file) gives the row "W × H px", and once the width differs from the asset's by a twentieth of a percent or more, " · " and the percent to one place at most, ties to even, in the user's locale as Swift's `.formatted`; a blank layer has no pixels to measure. Tests: `TransformInspectorTests::cancelAndApplyShowOnlyWhileAnEditWaits` (a drag's edit unseen, its click falling through and Return applying; a waiting edit fading in, clicked; Cancel), `theFieldsTakeTheSpareRoom` (the row's place), `NativeResolutionPaintTests::layersPanelSaysHowFarALayerIsScaled` (Swift's, the threshold either side, a rounded tenth, a blank layer), the row in `NativeLayerListTests`. Mutants (13.11): paint refusal 8 (2 survivors pinned by a folder's mask and an adjustment's mask, 2 equivalent as above), gradient 2, Smear 1, Inverse 1; `blurSample` 19 + 5 reruns (the unbuildable `1.0` replaced by `abs` for `sqrt`; the determinant's sign pinned by a layer turned 45°, the coarse sample's placement, size and σ × fit by an exact comparison of a 4200 × 3900 layer's sample), one survivor left untested: the `maxSurfacePixels` cap on the budget, reached only past a 50-million-pixel canvas with an 800 MB sample; `cloneSample` 4, `clonePixels` 3 (the scale pinned by `aGridCloneIsScaledOntoItsPlace`), `gridRect` 2, `beginBrush` 2 (a blank layer's Blur begins no stroke); `TransformInspector` 14, equivalent: the `pending == m_pending` guard (only restarts a fade toward the same end) and `setStartValue` (an animation without one starts from the property's value); `sizeLabel` 6 + 2 reruns, all caught. - Masks as Photoshop's (13.12, `b3419ab`): `addMask` with a selection now reveals it (white inside, black outside, the coverage as drawn) and, `revealing` false, hides it (the coverage inverted), one `Reveal Selection` or `Hide Selection` step; the row menu's Reveal All and Hide All call it as before, so Hide All with a selection hides the selection. `LayerMaskMenu` reads Alt from `QGuiApplication::keyboardModifiers()` when clicked, Swift's `NSApp.currentEvent`, so Alt-click adds the opposite, with Swift's help in Alt's words. The session keeps `viewsMaskAlone` (a getter and a setter that notifies on a change, the badge's write; `EditorSession.h`, defined in `EditorSession.cpp` beside Swift's `didSet`) and `maskAloneLayer()` (Swift's computed property: the view asked, the mask targeted, the active layer with a mask); `isMaskSelected` gains Swift's `didSet` as a private `setIsMaskSelected` (declared in `LayerMask+Session.h`, since `EditorSession.h` sits at 499 lines; defined in `EditorSession.cpp`), which every former write of `m_isMaskSelected` now calls, so targeting the pixels, another layer, a deleted mask or a load ends the view. `maskAloneLayer` and `toggleMaskAlone` live in `LayerMask+Alone.cpp`, since `LayerMask.cpp` already held 494 lines. `toggleMaskAlone` targets the mask through `selectLayerTarget` and shows it unless it already showed, refusing where that refuses. The canvas's `DisplayState` gains `maskAlone` (the mask asset's `ImageIdentity`, whether or not enabled, since the composite may skip it), and `drawLayers` hands off to `drawMaskAlone` (`EditorCanvas+Masks.cpp`, beside `liveFolderMaskClip`, which moved there to keep `EditorCanvas.cpp` under 500 lines): the canvas filled with the mask's edge tone, then a brush stroke or gradient laid into that mask through `LayerRenderer::drawBrushPreview` (the painted mask's own raster when it has one), else the mask at its displayed placement or the layer's transform, gray where it shows; the crisp zoom path draws through `drawLayers` too. The list's mask thumbnail: Alt's release without a drag calls `toggleMaskAlone` (Shift no longer toggles there, as Swift's), the tooltip names Alt-click, and the targeted ring is white while that mask shows alone (`setTargeted`'s `alone`; Swift's `maskAloneLayer?.id == layerID` is left out as dead: the ring shows only on the active row, whose layer is the one shown alone). `LayerListFocusTests` and `LayerDragGestureTests` expected Alt-Shift to toggle the mask; they now expect Swift's view. `MaskAloneBadge` (in `UI/LayerMaskMenu`, Swift's file) is 26 points tall, 75% black with a 14% white rim, radius 13: the mask glyph at 11, "Layer Mask" 12 px semibold, the layer's name 12 px at 60% white, elided within 220 points, and an `xmark` button (a new `LayerIcon`, Swift's bold `xmark` as two round-capped strokes) with Swift's tooltip in Alt's words and accessible name, which ends the view. `ContentView` keeps one badge, shown while `maskAloneLayer` holds, in a layout nested in the canvas slot (it starts hidden through `synchronize`, which the constructor runs) (bottom, centred, 14 points up), so the room beside and below it stays the canvas's; Qt child widgets take their own clicks, so Swift's AppKit hosting has no twin. Swift's GPU fix (a mask stroke's grid starting at the mask's background) is the twin's behaviour already: `allocateTile` fills a mask tile with `maskBackground`. Tests: `MaskAloneTests` (Swift's three, plus another layer ending the view and the setter's signal, a stroke and a gradient shown as committed, a disabled and a placed mask, the crisp zoom, the badge's words, place, elision and clicks, the list's Alt-click, Shift ignored and the white ring), `SelectionEditTests` (Swift's renamed two and the scaled layer's hiding mask), `LayersPanelTests::theMaskButtonUsesTheSelection` (plain and Alt clicks with and without a selection, the tooltips, the row's Hide All). Mutants: the session 10 + 1 rerun (the active-layer term of `toggleMaskAlone` pinned by a busy refusal with another layer's mask targeted), equivalent: `maskAloneLayer`'s `m_isMaskSelected` and `layer->mask` terms (every path that clears either clears the view through `setIsMaskSelected`; Swift's guards, kept); `drawMaskAlone` 6 + 3 reruns (the placement pinned off the old box), equivalent: the stroke's `isMask` and layer terms (a stroke while a mask shows alone is always that mask's: the view needs the mask targeted, and every target switch commits a pending gradient); the canvas branch and state 2; the list 4 + 2 (a release off the thumbnail pinned); the button and badge 5 + 3 (margins and spacing pinned); `ContentView` 5 (the constructor's `hide` was dead and is gone). All caught otherwise. +- Quit mid-edit and Scanlines (13.17, Swift 1.4.4). Quit (`1aad384`): `ProjectWorkspace::settlePendingEdits` twins Swift's: on the tab on screen a pending gradient is applied (`commitGradient`), then a pixel move (`finishPixelMove`), then a filter, Hue/Saturation, Levels, Color Range, the Expand/Contract/Feather panel (`setSelectionAmountOperation(nullopt)`) and the colour picker are cancelled (their Cancel, so nothing un-OK'd lands). It takes a completion callback (Swift's `async`), run from the event loop once both commits have landed, and only while the workspace lives (`QPointer`), the tab kept alive in the callbacks as every workspace flow keeps it. `confirmQuit` refuses at once while the workspace manages (Swift's guard moved there from `applicationShouldTerminate`; the twin's window close reaches `confirmQuit` directly, so this guard is what stops a second close mid-quit), applies open text, settles, and only then asks `canSwitch`; a refusal there (a busy project, a commit still running) is a warning on `compositor.app` where Swift beeps. Swift's `gradientEdit != nil` and `pixelMove != nil` tests before awaiting are left out: both twins return (posted) when there is nothing to commit. Swift's `finishAdjustmentEditing(commit: false)` is left out as dead: `cancelFilter`, `cancelHueSaturation` and `cancelLevels`, each called before it, call it first, and an adjustment's editor is always one of those three (its request and original are set and cleared with them; while the editor still renders, `finishAdjustmentEditing` does nothing in Swift either, and `canSwitch` refuses the quit). Tests: `QuitPendingEditsTests` (Swift's `quittingAppliesAPendingGradient` and `quittingCancelsAnOpenDialog`; a pixel move applied as one `Move Pixels` step; Color Range putting the selection back, the selection-amount panel, the colour picker not taking its colour; an adjustment's editor putting the settings back; a workspace gone mid-settle calling nothing; closing the window applying the gradient before the Save question; a busy project refused with the warning, nothing asked or applied; a second quit while the first asks refused unsettled). Scanlines (`9c99853`, `cd2f998`): `DitherStyle::scanlines` ("Scanlines (CRT)") joins the fourth group; `usesPixelSize` (false for ASCII and Scanlines: no chunky pixels, no Pixel Size or Pixel Shape rows) and `drawsMarks` without Scanlines, as Swift's. `DitherSettings` gains `lineSpacing` (2–32, rounded, 4), `glow` (0–100, 35), `dots` (0–100, 0) and `wobble` (0–64 px, unrounded, 0), normalized as Swift's; the kernel's cell is the line spacing for Scanlines. `kernels/DitherPixels.c/.h` are cd2f998's, with one adaptation: Swift's kernel threads through GCD (`dispatch_apply`) and Apple blocks, which GCC cannot compile. `in_bands` keeps Swift's split (one band under 64 items, else 32) but takes a C function and a context; the three block bodies (the tone pass, the scanlines, the glow) are moved word for word into static functions with context structs (`tone_job`, `scan_job`, `glow_job`), and `__block int failed` is a C11 `atomic_int`. The bands run through `dither_bands`, a C hook defined in `Document/Dither.cpp` on `PoolMap::blocking` (QtConcurrent, so a filter worker lends its pool place). The arithmetic is unchanged; this is a forced difference (GCC has no blocks or GCD), agreed with the coordinator. `bandsDrawAsOneBandDoes` holds 32 bands against one: the top rows of a 300-row picture equal a 62-row picture's, which runs as one band in both passes. `glowing` twins Swift's: sigma `lineSpacing × 3 + 3`, shrunk by `max(1, floor(sigma / 4))`, blurred at `sigma / shrink`, scaled back and added through `dither_glow` at `glow / 100 × 2.5`. Core Image clamps to the extent before it shrinks, so the twin pads the image by the blur's reach (three sigmas, a whole number of shrink steps) with its edges repeated, shrinks that with `QImage::scaled`'s smooth filter (an area average, where Core Image samples bilinearly), blurs with the edge repeated (`PixelAdjust::gaussianBlur`), scales back bilinearly and crops the middle; it stays within three levels of light of Swift's first commit's full-size blur (`theGlowIsTheBlurredLightAtItsAmount`, both axes). The sheet adds Line Spacing (px), Glow (%), Dots (%) and Wobble (px) with Swift's help under Scanlines, and Pixel Size and Pixel Shape follow `usesPixelSize`. Tests: `ScanlinesTests` (Swift's four `DitherTests`; the beam's rows for white, 0.35 gray and a striped average; the style's flags and that pixel size, shape and cell size change nothing; the four settings' clamps, rounding and defaults; Two Colors' screen and Original's black; clear pixels; every band of tall images, 32 bands and a short last one; the glow against a full-size Gaussian at 20% and 100%, its fade, and alpha; the sheet's rows, help, slider ends and fields), `DitherTests` and `DitherSheetTests` (the eleventh style in names, groups and the menu). Equivalent mutants: a pad of one sigma for three (the padded edge repeats the image's own edge, so the light differs by under a level), the shrunk blur without edge repetition (the pad keeps its reach off the edge), the kernel's `start < end` (a band past the end runs no line) and its band count (it only spreads the work). `settlePendingEdits` calls `done` directly: it already runs from the session's posted callback, after both commits. Untested path: the kernel's scan row that cannot be allocated (`failed`, a few kilobytes per band). Lead told of the GCD adaptation before it was made. the kernel cannot be copied unchanged (GCD and blocks); its threading is adapted, its arithmetic kept. Swift's beep on a refused quit is a warning on `compositor.app`. `SheetTeardownTests` called `CanvasSizeSheet` with its arguments from before 13.4a; fixed at the merge (a session passed), and it still fails when the sheet's `releaseFocus` goes (mutant caught). +- Camera Raw curves as Photoshop's (13.18, Swift 1.4.5: `60d9117`): `kernels/AdjustPixels.c/.h` are 60d9117's: the tone curve runs on red, green and blue alike (`toneLut`, Swift's rename of `lumaLut`), and Refine Saturation means what Swift's now means: 0 the curve per channel, below 0 easing toward the colour scaled to the curved brightness (−100 brightness alone), above 0 pushing the curved colour from its luminance by 1 + the amount. `CameraRawCurveSettings::parametric` twins Swift's: unchanged with all four amounts at 0; otherwise 33 anchors, each `bend` of `bend` (shadows and highlights past the outer dividers, then darks and lights either side of the middle one, gamma curves of strength 1.66), run through the same curve as Image › Curves (`point`), so the regions meet without a corner. `bend` is a private static, as Swift's; `lumaTable` is `toneTable`. The twin's normalization of the dividers (5–90, +2 apart, 98 at most) still clamps what a drag writes. The graph's gestures (`UI/CameraRawColorControls+Curve.cpp`, beside the file that would pass 500 lines) are Swift's single `DragGesture`: a left press picks once (`parametricDrag`: within 18 points of the bottom the nearest divider, the first of equals; elsewhere the region whose tone range holds the press, a tone on a divider joining the region above; `pointDrag`: the point nearest by distance within 0.055, chosen; else a new point there, if fewer than 16, inside x 0.01–0.99 and over a hundredth from every point's x, sorted in, stored and chosen), then `continueDrag` runs at the press and on every left-button move: a point's output follows the pointer, an inner point's x stays a hundredth inside its neighbours, an end keeps its x; a divider follows, shadow below dark − 2 and dark below light − 2; a region's amount is its start minus the drag's height over the graph's times 200, within ±100, rounded. Left out as dead, since normalizing does each (`CameraRawCurveSettings::normalized`: amounts, dividers 5–90, +2 apart, 98 at most, and the repair's 0–1 outputs): Swift's 2–98 clamp of a divider, dark's `shadow + 2` floor and light's `dark + 2` floor, and the 0–1 clamps of a dragged or new point's output (a press inside the graph is inside 0–1 anyway). The new point's `x > 0.01` and `x < 0.99` are dead too: the ends sit at 0 and 1, and a point within a hundredth of any x is refused. Swift's explicit refresh after the press is dead: every press writes the settings, which announce. `bend`'s `lower > 0` and `upper < 1` are dead (no tone lies below 0 or above 1). The release lets go. Qt can lose a release, so every press picks afresh. A double click is Qt's second press: it picks and drags as a press (Swift's drag sees it too), then on the point page removes the nearest inner point by x within 0.04 and drops the choice, and lets go of the drag (Swift removes at the tap's end, which ends the drag). Swift re-picks on each move while a press picked nothing; with the same start and points it picks nothing again, so the twin leaves that out. The chosen point (Swift's `selected`) draws in `Highlight`, the accent, and the readout shows only while it exists, following the drag; another channel drops the choice and the drag (Swift's `onChange`). The graph's help and Refine Saturation's are Swift's new words. Swift's `CameraRawMixerControls` gains the same `Drag` and `selected` state but never reads it: dead, left out. A fix beyond Swift: Swift holds an inner point exactly a hundredth from its neighbours, where the curve's repair (`x > kept.x + 0.01`) drops it (or drops the next point), and the drag's index then names another point; the twin holds it a billionth further inside, so the repair keeps it and the drag stays on it (`aPointIsPickedByDistanceAndKept`, `endPointsStayAndARealDoubleClickRemoves`, both failing before the fix). A fix beyond Swift: a preset's points drop the choice (TASKS asks for it; Swift's `applyPreset` keeps the index, which then lights whatever point holds it); Custom changes nothing and keeps it. Tests: `CameraRawCurveTests` (Swift's `parametricCurveIsSmooth`, `parametricCurveMatchesPhotoshop`, `curveDeepensColorLikePhotoshop`; each anchor against the bend written apart, and the smooth curve between; each region bending only its range, dividers at the ends bending nothing; the tone table through the point curve; Refine Saturation at 0 per channel through the table, −100 brightness with the hue kept, −50 halfway, +50 half again from luminance), `CameraRawCurveGraphTests` (regions lifted and lowered from their start, clamped and rounded, picked once by tone; dividers in the strip, nearest and first of equals, kept ordered and within the settings' limits; points by distance, kept through the drag, a new point added and dragged by the same press, ends moving their outputs alone; refusals near an edge, a point's x, at sixteen; the chosen point lit and read out as it moves, ends and new points included; a channel or preset dropping the choice, Custom keeping it, a held drag let go; double clicks removing an inner point and the choice, adding then removing on empty ground, ends staying; other buttons, a lost release, a release letting go; the help), and the existing `CameraRawColorTests`, `CameraRawCurveMixerTests`, `CameraRawGroupDrawTests`, `CameraRawGroupRowsTests`, `CameraRawGroupWordsTests` brought to the new rules and words. Equivalent mutant: `parametric`'s shortcut when all four amounts are 0 (33 anchors on the diagonal give the diagonal; it only spares the work). The kernel's refine branches, Swift's own code, were mutated too and all caught. Swift's `selected` survives a preset; TASKS asks for a preset to clear it, and a stale index lights a point the user never chose, so the twin clears it (a fix beyond Swift). An inner point dragged onto a neighbour vanished in Swift; fixed beyond Swift at the coordinator's request. - Drops between projects: `ProjectWorkspace::draggedLayerSource` finds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag's `QMimeData`, which every drop target has), `canReceiveDrag(data, destination)` refuses only a layer's own tab, and `receiveProviders` copies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does. `ContentView` takes such a drop over its whole area (`setWorkspace` hands it the workspace; `acceptsDrop` keeps Swift's guards: busy, the sheet, the importer, a rename; `levels` arrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas. `ProjectTabButton` takes drops for its tab (accent fill and rim, "Add to