diff --git a/AGENTS.md b/AGENTS.md index e5d7076..ea2cc4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,7 +117,7 @@ Platform adaptations, each forced by a missing Qt analog: - Exports (10.3): `IO/ProjectController+Export.cpp` holds `exportPNG` and `exportJPEG`, each with a completion callback. `exportPNG` asks for a document and `begin` (no window: its panel then stands alone), takes the snapshot, opens the panel and renders, encodes and writes in one worker (`exportWork`, which catches `std::runtime_error`, the exporter's errors and the write's); `exportJPEG` also needs the window, renders in a worker (catching `ExportError`), shows `JPEGExportSheet` in the window-modal `sheet` of 10.2, then the panel, then writes the sheet's bytes in a worker. The project stays busy until the file is written, the flow is cancelled or its alert is dismissed, Swift's `defer`; a snapshot that finds no memory is explained as the render error, and `ImageExporter::render` and `jpeg` turn `std::bad_alloc` into `ExportError(render)` at their doors (function-try-blocks), which also serves 9.5c's editor render; the PNG snapshot's copy for the worker and `exportWork`'s launch are guarded too, explained as the render error. The launch's guard is not reached by the tests: its few small allocations fail only under a limit that fails the alert as well. The panel's callback and `showError`'s are moved, never copied, since the PNG flow's completion holds the snapshot. `exportPanel` is Swift's `NSSavePanel` for one type: a `QFileDialog` titled Export PNG or Export JPEG with the type's name filter and default suffix, suggesting the project's name (`ProjectTab::nameWithoutSuffix`, Swift's `deletingPathExtension`) or Untitled with `.png` or `.jpg`; a chosen name without one of the type's suffixes (`png`; `jpeg`, `jpg`, `jpe`; any case) gains the preferred one (`png`, and `jpeg`, `UTType.jpeg`'s preferred extension), where the Mac panel enforces the type, and as with Save As a completed name where anything exists is refused with an alert. `ImageExporter::jpeg` takes Swift's three cancellation points as an optional `cancelled` test, throwing `CancellationError` (Swift's type) before the flatten, before the encoding and before the preview. `UI/JPEGExportSheet` twins Swift's sheet: the title, the 560 by 330 `JPEGPreview` on gray 0.12 (13.4b; while updating, a 64-point plate of the window colour at 85%, Swift's regular material, behind a 32-point busy `QProgressBar`), Quality as a slider in hundredths with its percent (`lround`, Swift's `rounded()`) 45 points wide and right-aligned, the matte as a label and a `DialogColorSwatch` on the app's picker (13.4a; the same colour asks nothing, as Swift's `onChange`), then one row of the raster's size grouped with "px · sRGB", the error in `BrightText`, or the file's size, or "Updating…", and Cancel and the default Export…, enabled only while a ready result stands. Swift's `.task(id: options)` is `request`: it sets the last request's flag, clears the error and restarts `jpegWait`, a 200 ms single-shot `PreciseTimer` (Swift's sleep), whose timeout encodes in a worker holding the new flag; a result or error lands only while its flag is clear, Swift's check after the await; the destructor sets the last flag, as SwiftUI cancels the task with its view. Export stores the quality under Swift's key in `QSettings`; a stored value counts when it reads as a finite number, clamped to 0–1. `UI/ByteCounts` is Linux-only: Foundation's `ByteCountFormatter` in its memory style (binary) and file style (decimal), shared by the canvas sheet and the JPEG sheet: bytes grouped, KB whole, MB to one place, GB to two, trailing zeros dropped. The File menu gains Export PNG… (Ctrl+Shift+E) and Export JPEG… (Ctrl+Alt+Shift+S) after Save As, enabled with a document while the controller can start. Copy Merged came with 7.4. Tests: `JPEGExportSheetTests`, `ProjectExportTests` (the real panels and alerts from `DialogDesk`, names typed in full since a panel opens where the last one went, and the File menu's entries), `ProjectExportFailureTests` (a binary of its own: the render's door, the snapshot, the JPEG worker; `DialogDesk::aroundAccept` sets a limit around a panel's accept alone, since a dialog that paints under one can fail in FreeType and abort; the project holds 2,048 layers and the panel limits sit at 5,120 KB, between the snapshot's peak of 3,456 KB and a copying mutant's 6,912 KB, measured in 12.16, because the heap's free chunks count on top of a limit and shift with load; `projectSnapshot` reserves its record list, since a growing list held two at once (5,120 to 5,376 KB at its peak, which made the old 2,560 KB limits for 1,024 layers fail one run in three), and `aSnapshotHoldsOneRecordList` holds that at 4,608 KB; `limited`'s `malloc_trim(0)` is an equivalent mutant, since no free top chunk serves a mapping that size), `ByteCountsTests`, the cancellation points in `JPEGExportTests`, the shortcuts and the count in `CompositorMenusTests`. Equivalent mutants: the options a result lands with taken as the current ones (only the newest request lands, and its options are current), the `!m_error` terms of the size's visibility and of Export (an error follows only a failed encoding of options that never became ready, and encoding is deterministic), and the destructor's flag (it only stops a worker whose result nobody reads). - File and picture drops (10.4): `IO/ImageFileDrop` twins Swift's enum as a namespace. `importProviders` takes the drop's `QMimeData`, where Swift takes item providers: each local file URL is a file, in order; a URL that is no file counts as unreadable, unless no file came at all and the drop holds a picture (a browser's or a screenshot's), whose bytes are copied to `Dropped-` in the temporary folder, as Swift's `temporaryFile` copies a promised file, the format chosen in Swift's order (PNG, JPEG, HEIC, TIFF, then any `image/` type) and the suffix from Qt's MIME database. The files go to the workspace's `receive` or, without a workspace, to the session's `importImages`; once that has run, a drop with anything unreadable adds Swift's words to the session's import error, after the importer's own ("from the file manager" where Swift says "from Finder"). Swift's `projects:` parameter has no caller and is left out. `holdsImages` is Swift's drop types but the layer row's: a local file or a picture. `receiveProviders` copies layer rows as before and hands anything else to `importProviders` one provider at a time (`providers`: a URL each, or, when no file came, one item holding the preferred picture alone, so a lazy drag is asked for nothing else, as Swift asks for one format; its web addresses are left behind, since alone they read as unreadable either way), each after the last has run, for the tab then in front, whose session hears what could not be read and stays alive until its import has run; so an import error holds the next file until it is dismissed, Swift's gate between providers. A copied picture that cannot be written whole (its flush checked, since `QFile` buffers) is removed and counts as unreadable; a failed flush fails `QFile::remove` too, so the copy is closed and unlinked by path. An open alert shows its message as it stands (`showAlert` updates it), as SwiftUI's alert re-reads it. The canvas, the tab buttons and the New canvas button take files and pictures as they take rows (a canvas without a workspace under Swift's session guards, Levels, busy, the new-canvas sheet, the importer and a rename, importing into its own session); Alt refuses only a row, as Swift's `validateDrop`. Tests: `ImageFileDropTests`. - Slider snapping (10.5): `UI/SliderSnap` twins Swift's enum as a namespace. Swift swaps `NSSliderCell.startTracking` so that a press on the track sets the value whose knob is centred there before tracking begins; the twin's `install` puts one application event filter in place, once, as Swift's lazy static swaps once: a left press on an enabled horizontal `QSlider` with a range, outside its knob and with travel for it, sets the slider down, then sets the value under the press (`QStyle::sliderValueFromPosition` over the groove less the knob, the knob's half-width off, flipped for an inverted slider), and presses where the knob now is, the original press consumed, so Qt drags the knob from there even when the slider's owner rounds the value and moves it (a filter's field); a press made with another button held is left to Qt, which refuses it. So editing begins before the value, Swift's order (the opacity slider's drag opens its one step first), and a press on the knob drags it from where it is. QSlider keeps its style option protected, so the filter builds one as `initStyleOption` does. Qt's own absolute set (`SH_Slider_AbsoluteSetButtons`) would set the value before the press and recentre a pressed knob, so it is not used, but it is the test's oracle for where the knob lands. `main` installs it after the theme. Tests: `SliderSnapTests` (Swift's `clickingTheTrackSnapsTheKnobAndStillEditsTheValue`: the value under the press at once, editing begun first, a drag going on, the release; the knob, other buttons, a disabled, vertical, empty, inverted and narrow slider). Equivalent mutant: the groove's own offset, zero under Fusion, the app's one style. The call in `main` is not under test, as the theme's construction there is not. -- Selection modifiers, Select Subject, Soft Light and the font menu (12.1, Swift 1.1.1–1.1.4): `DocumentSelection` gains `feather`, the fade's reach in document pixels. Its coverage is filled antialiased and blurred by `PixelAdjust::gaussianBlur` at sigma feather / 2 with the edge repeated, Core Image's `clampedToExtent`; `coverageBounds` grows the outline's box by `ceil(2 × feather)`, and the clip, the copy region and a mask made from the selection (now drawn through `PixelAdjust::coverage` from the clip) take it. A feather stacks as blurs do (the root of the squares, 250 at most) and travels through moves, Expand, Contract and Inverse. The Select menu's Expand…, Contract… and Feather… ask for an amount (`promptSelectionAmount`); `selectionAmountOperation` is a getter and a setter that resumes file requests, read first by `canEditLayers`, `canUseHistory` and `canStartProjectOperation`. `SelectionAmountSheet` (in `LassoControls`, Swift's file) is Swift's sheet in a `FloatingPanel` named `selectionAmountPanel`, titled by the operation; its note keeps its room while hidden, as SwiftUI's opacity; `QString::toInt` trims spaces as Swift's `trimmingCharacters` does. The selection bar gains Feather and its amount (1–250). Select ▸ Subject (Ctrl+Alt+A) draws the live composite, runs `SubjectRemoval::subjectMask` in a worker with the project busy (a completion callback, as the other jobs), and on the same document applies `MaskTracing::whitePixels` (at least half gray) as the selection, one `Select Subject` step, or sets `brushError`; `canSelectSubject` leaves out Swift's document and busy terms, which `canEditLayers` reads. Soft Light joins `LayerBlendMode` after Overlay as QPainter's `CompositionMode_SoftLight`, which follows the PDF formula `BlendModeTests` holds. `TypeFontPicker` is Swift's: the style's own name alone until the menu opens, a missing name added at the end, an open menu left alone, the face already shown writing nothing, the current name elided at its end within 210 points (Fusion's label draws a pixel in from each side of the edit field). Point text caches its measure by style in the inline editor, and `setTextDraft` resumes file requests only when a draft ends, as Swift 1.1.3's. Swift's `Geometry` cache, which skips AppKit's frame work, has no twin: the canvas draws the editor. Equivalent mutants: the editor's box branch (`textBoxSize` returns the box), `setTextDraft`'s ended test (`resumeFileRequests` asks `canStartProjectOperation`, which a draft refuses). Tests: `SelectionFeatherTests` (Swift's, then stacking, travel, bounds and the sheet's session half), `SelectionAmountSheetTests`, `SelectSubjectTests`, `TypeFontPickerTests`, and the menus, blend modes and Type bar in their files. +- Selection modifiers, Select Subject, Soft Light and the font menu (12.1, Swift 1.1.1–1.1.4): `DocumentSelection` gains `feather`, the fade's reach in document pixels. Its coverage is filled antialiased and blurred by `PixelAdjust::gaussianBlur` at sigma feather / 2 with the edge repeated, Core Image's `clampedToExtent`; `coverageBounds` grows the outline's box by `ceil(2 × feather)`, and the clip, the copy region and a mask made from the selection (now drawn through `PixelAdjust::coverage` from the clip) take it. A feather stacks as blurs do (the root of the squares, 250 at most) and travels through moves, Expand, Contract and Inverse. The Select menu's Expand…, Contract… and Feather… ask for an amount (`promptSelectionAmount`); `selectionAmountOperation` is a getter and a setter that resumes file requests, read first by `canEditLayers`, `canUseHistory` and `canStartProjectOperation`. `SelectionAmountSheet` (in `LassoControls`, Swift's file) is Swift's sheet in a `FloatingPanel` named `selectionAmountPanel`, titled by the operation; its note keeps its room while hidden, as SwiftUI's opacity; `QString::toInt` trims spaces as Swift's `trimmingCharacters` does. The selection bar gains Feather and its amount (1–250). Select ▸ Subject (Ctrl+Alt+A) draws the live composite, runs `SubjectRemoval::subjectMask` in a worker with the project busy (a completion callback, as the other jobs), and on the same document applies `MaskTracing::whitePixels` (at least half gray) as the selection, one `Select Subject` step, or sets `brushError`; `canSelectSubject` leaves out Swift's document and busy terms, which `canEditLayers` reads. Soft Light joins `LayerBlendMode` after Overlay (since 13.9a hand-blended by Photoshop's formula, see that bullet). `TypeFontPicker` is Swift's: the style's own name alone until the menu opens, a missing name added at the end, an open menu left alone, the face already shown writing nothing, the current name elided at its end within 210 points (Fusion's label draws a pixel in from each side of the edit field). Point text caches its measure by style in the inline editor, and `setTextDraft` resumes file requests only when a draft ends, as Swift 1.1.3's. Swift's `Geometry` cache, which skips AppKit's frame work, has no twin: the canvas draws the editor. Equivalent mutants: the editor's box branch (`textBoxSize` returns the box), `setTextDraft`'s ended test (`resumeFileRequests` asks `canStartProjectOperation`, which a draft refuses). Tests: `SelectionFeatherTests` (Swift's, then stacking, travel, bounds and the sheet's session half), `SelectionAmountSheetTests`, `SelectSubjectTests`, `TypeFontPickerTests`, and the menus, blend modes and Type bar in their files. - Folders, opacity and Snap (12.2a, Swift 1.1.5–1.1.6): a folder takes an opacity that multiplies into each layer inside it, never composited as a unit, since folders pass through. `LayerOpacity::effective` (in `LayerGroups.h`) walks up to 64 folders, a lookup standing for Swift's closure; `ImageLayer::effectiveOpacity`, `ProjectLayerRecord::effectiveOpacity` (declared in `ProjectStore.h`, defined in `LayerGroups.cpp`, as Swift's extension) and `CanvasDocument::effectiveOpacities` twin Swift's, the last refusing a repeated id with a `logic_error` where Swift's dictionary traps. Every drawing path takes it: the export, `drawLiveComposite`, the canvas (its every draw, adjustments included, and `DisplayState`, so a folder's change redraws) and the live-mask baker's sources, whose records are pointers, so it calls `LayerOpacity::effective` itself. `canEditOpacity` is one layer, a folder included; `canEditAppearance` stays for blending, so `LayerAppearanceControls` rests with `canEditOpacity` and its Blend caption and picker with `canEditAppearance`; `setSelectedLayersOpacity` dims selected folders too. The format moves to version 8 here (Swift's guides arrive under it in 12.3; until then a version-8 project's `guides` key is ignored as unknown), because 1.2.0's store rule (`d9b3f9e`) lets a folder dim from version 8 on, its blend mode always normal. View ▸ Snap is `snappingEnabled`, a getter and a setter that clears the guides and notifies (Swift's `didSet`); `snappedMove` and the crop drag read it. The transform box draws its accent alone. The effects sheet's sliders span Swift's new ranges (Size 0–20, Distance 0–100, the inner shadow's 0–50, Blur 0–100) while typing and steps reach the effect's own limits (500, 5,000, 500), clamped by `apply`; `QSlider` pins a larger value at its end by itself, so Swift's clamp of the thumb has no twin; the swatch takes a white ring inside a black rim, where SwiftUI's `contentShape` has no need of a twin (a Qt button takes its whole rect). The layer list takes T for Type, as 1.1.6's table does. Equivalent mutant: `canEditOpacity`'s `activeLayer()` term (one selected layer is the active one). `drawPixelGrid` moved to `EditorCanvas+Grid.cpp`, keeping the canvas file under 500 lines. Tests: `FolderOpacityTests` (the walk, the export, the composite, adjustments, effects, the canvas in each way it draws, the baker, Swift's `aDimmedFolderSavesAndReopens`), folder duplication in `LayerDuplicateTests` (moved out of `EditorSessionTests` at 500 lines), `TransformTests` and `LayerDragTests`, the controls in `LayerAppearanceControlsTests`, Snap in `TransformSnapTests`, `CropCanvasTests` and `ViewMenuTests` (the View menu tests, moved out of `CompositorMenusTests`), the ranges and swatch in `EffectsPanelTests`. - Keyboard shortcuts (12.2b, Swift 1.1.6): `UI/KeyboardShortcuts.h/.cpp` twin Swift's file, `KeyboardShortcuts+Sheet.cpp` its sheet and recorder. `ShortcutChord` keeps Swift's key strings (Backspace 0x7f, Return, Escape, Tab, Space, the arrows as Cocoa's F700s) and modifier bits, Ctrl for Command, Alt for Option, Meta for Control, then Shift; a key event reads Backspace and Delete alike, Enter as Return, Backtab as Tab, a shifted bracket or sign as its own key, anything else by its lowered key name, so a function key's name is long and refused (Swift takes it as a private-use character; known difference); labels are `QKeySequence`'s native text (Ctrl+Alt+K), where Swift prints symbols. `ShortcutDefinition::all` is Swift's list less Hide Compositor, which has no twin; `problem` keeps Swift's rules less its three chords reserved by macOS (the desktop takes its own before the app does). `ShortcutSettings` keeps the overrides in `QSettings` under Swift's `keyboardShortcuts.v1`, as Swift's JSON, a stored set that fails `problem` ignored with a warning on `omaphoto.app` where Swift is silent; `changed()` stands for its observation, and `reload` reads the store again, as a launch does. The menus remember each entry's own key in a property and take `menu` of it on every change. `canvasEvent` and `textEvent` hand back the press to handle, or none to swallow: the canvas (its key handling moved to `EditorCanvas+Keys.cpp`, the navigation file at 500 lines), the layer list and the window's key filter read `canvasEvent`, open text `textEvent`; a translated press the canvas or list leaves passes the original on, as `super.keyDown` does. Swift's `panPhysicalKey` is `m_panKey`: the key whose release ends a remapped pan. `NativeShortcut` is `configuredNativeShortcut`: a `QShortcut` over the sheet and its children clicks its button when that shows (a disabled button refuses `click` by itself); its own plain Return or Escape rests it, leaving the dialogs' default and escape handling, so a field's Return still commits first, while a button moved onto the other's key takes it; a moved original key is swallowed at the sheet; a synthesized press carries Swift's shifted text (`}` for Shift+]), which the brush keys read; `NewCanvasSheet`'s own Return shortcuts rest while Apply is moved; Levels' Preview takes its chord this way, in place of the checkbox's shortcut. The sheet is Swift's: 660 wide, the hint, a search, the three groups of rows with 150-by-26 recorders, the contextual words in Linux's keys, the problem in `BrightText` at 12 pixels only while there is one, Restore Defaults, Cancel and a default Save resting while recording or wrong; a modifier alone keeps recording, and while recording the recorder claims every key, Swift's `performKeyEquivalent`: a `Qt::Tool` panel keeps its parent window's menu shortcuts live, and `QWidget::event` would move the focus on Tab. It opens from the Edit menu, after Paste and a separator, in the `keyboardShortcuts` panel `CompositorMenus` holds. Equivalent mutants: `menu`'s and the Shift branch's `isMenu` terms (menu chords all carry modifiers), `canvasEvent`'s early return without overrides, `m_panKey`'s reset (every pan sets it) and Save's explicit default (its only auto-default button). Tests: `KeyboardShortcutsTests`, `KeyboardShortcutsSheetTests`, `RemappedKeysTests`. - Guides and the layout grid (12.3a, Swift 1.1.7): `Document/Guides.h/.cpp` twin Swift's `Guides.swift`: `CanvasGuide` (its axis by Swift's raw values, `rawValue` and `guideAxis`), `LayoutGrid` (a struct since 13.5b), `GuideDrag`, and the session extension, declared in `Guides+Session.h` with the accessors of Swift's view flags (`showsGrid`, `showsGuides`, `showsRulers`, `snapEnabled`, the four Snap To flags, `locksGuides`, each a getter and a setter that notifies, and `guideDrag`), whose state stays in `EditorSession.h`; `guideHitDistance` and `guideColor` are Swift's statics. `finishGuideDrag` takes `removing` where Swift names it `delete`. `canEditGuides` leaves out Swift's `_ = showsBusy`, which only registers an observation. 1.1.7's `snapEnabled` (Snap To's master switch, read by `alignmentSnapTargets` and `snappedGuidePosition`) stands beside 1.1.6's `snappingEnabled` (read by `snappedMove` and the crop drag), as it does in Swift through 1.3.3, with a View entry each. `transformSnapTargets` and `cropSnapTargets` are `alignmentSnapTargets` with and without centres, the canvas's middle after its edges and a layer's between its edges, Swift's order; `TransformSnap::box` (in `Crop.cpp`) is the upright box both files read. The document holds `guides`, equality and undo included; the manifest's optional `guides` is Swift's `Codable` (an array of `id`, `axis` and `position`, left out when there are none, the snapshot writing none for an empty list), and the store refuses guides before version 8 (invalid), more than 1,000 (too large), a repeated id or a position past a million (invalid). `installProject` and `clearProject` drop a drag; `CanvasDocument(snapshot)` loads the guides, so `applyDocumentSize` and `applyImageSize` keep them; the canvas resizer offsets them, the image resizer scales them (a same-size resize keeps them), the canvas flip mirrors those across it. The View menu gains Swift's Show (Grid Ctrl+', Guides Ctrl+;), Rulers (Ctrl+R), Snap (Ctrl+Shift+;), Snap To (Guides, Grid, Layers, Document Bounds), Lock Guides (Ctrl+Alt+;) and Clear Guides, resting without a document or, for Clear, without guides; the five new shortcut definitions join the list. Equivalent mutants: `finishGuideDrag`'s `isNew` test on removal and its `original != position` test (the history drops a step that changes nothing). The rulers, the grid and guides on the canvas and their drags are 12.3b's. Tests: `GuideTests` (Swift's but the canvas drag and the ruler's steps), `ViewMenuTests`, the targets' order in `TransformSnapTests`. @@ -127,7 +127,7 @@ Platform adaptations, each forced by a missing Qt analog: - Outer Glow and brush Smoothing (12.5b, Swift 1.2.0): `OuterGlowEffect` joins the effects with Swift's defaults (20 pixels, white, 75%) and bounds (0–500, 0–1), `LayerEffectKind::outerGlow` ("Outer Glow") last, so the footer's menu, the list's rows and every helper take it; the manifest's `outerGlow` is Swift's `Codable` (`size`, `opacity`, the colour, `enabled` only while set). `LayerEffectsKernel` blurs the shape's coverage at sigma size ÷ 2, Metal's glow buffer (a size under 0.02 copies it, as Swift's zero shift does), and composes it over the drop shadow and under an outside stroke, clamped to glow × (1 − shape) × opacity; the renderer's margin takes three sizes, the stroke surface's reach three sizes and two. Swift's CPU `outerGlowCoverage` serves machines without Metal and is dead here, as the other CPU coverages are (9.6b), so Swift's CPU and Metal parity test has no twin. The preview cache keeps the last preview while one effect is hidden or shown on the same pixels (Swift's `dbe82b0`). A fix beyond Swift: a reduced preview scales the glow's size, as it scales the inner shadow's lengths (9.6c). The sheet shows the glow's swatch, Size (0–100 on the slider, 500 typed, px) and Opacity. Smoothing is Swift's string: `BrushSettings::smoothing` (0–100); while the Brush paints (either mode) with smoothing, the brush (`m_brushAnchor`) moves only once the pointer (`m_brushPointer`) pulls further than smoothing ÷ the viewport's zoom, and then by the excess along the string; the release paints on to the pointer; a cancel drops both. The other brush tools follow the pointer. Equivalent mutants: the press's pointer and the cancel's two resets (each makes the others' work redundant), `>` at the string's exact length (a move of zero), the release's tool and smoothing terms (otherwise it appends the last point again), and the surface's glow reach between the blur's three sigmas and Swift's three sizes. `BrushControls` shows Smoothing for the Brush alone: a 100-point slider in tenths and a 42-point `SelectionAmountField` of 0–100 with Swift's help. The rest of 1.2.0 is held already (TASKS.md 12.0, each note now saying Swift agrees) or not code (`NativeLayerList.moveLayer` has no caller; the spelling touches shader comments), but for two follow-ups of `90ee29f` and `7c4512a`: Select All never rests and reaches a focused layer list (7.1's note), and the invert test holds no wall clock. Tests: `OuterGlowTests` (Swift's, but the parity test), the glow in `LayerEffectsKernelTests`' shader replay, `LayerEffectsSurfaceTests`' rows and `EffectsPreviewCacheTests`' scaling, the kinds, manifest and menu in their files; `BrushSmoothingTests` and the bar in `BrushControlsTests`. - Camera RAW (12.6a, Swift 1.2.1): Core Image's RAW filter has no Linux twin, so `IO/RawImporter` develops through LibRaw (the user's decision), its thread-safe `libraw_r` found by pkg-config; `Dockerfile.dev` installs `libraw-dev`, the PKGBUILD and the flake depend on `libraw`, the distro images carry it. A RAW is any file whose extension names a type inheriting `image/x-dcraw` in the MIME database (`matches`, `isRawType` for a dropped picture, `globs` for the importer's filter), Swift's `UTType.rawImage`. `RawDevelopSettings` keeps Swift's fields, `isAsShot` and `reset`; `RawDevelopRequest` is Swift's tuple. White balance: `planckian` is Kim et al.'s cubic (1667–25000 K), `whitePoint` moves off the locus along its greener uv normal by tint ÷ 3000, `temperatureOf` inverts it by a mired search and golden sections: Adobe's scale and sign (the DNG SDK's `kTintScale`), so a positive tint is a greener white and turns the picture magenta, and D65 reads about +10, as Lightroom's Daylight. Known difference: CIRAWFilter's temperature and tint follow Apple's own model; these follow Adobe's convention on CIE 1960 uv. `asShot` reads the camera's multipliers (`cam_mul`, else dcraw's daylight `pre_mul`) and turns the neutral into xy through LibRaw's `rgb_cam` (a DNG's `cam_xyz` stays zero) with `pre_mul` put back; `develop` works back from the chosen white to camera multipliers (`user_mul`, the second green the first's), reading the camera's `pre_mul`, `rgb_cam` and size from unpack's copy (`rawdata`), since a process overwrites `color` with its own multipliers and a half-size one halves `sizes`; a white the matrix maps to a response at or below zero is refused, logged, where LibRaw would drop the balance in silence; the refusal is `ImageImportError(whiteBalance)`, a kind of Linux's own with words of its own, since `CIRAWFilter` takes every white and real cameras' matrices reach it inside the sliders (the warm and green corner). It takes LibRaw's 16-bit sRGB at a power of one (linear), with neither auto-brightening nor the maximum's adjustment (`adjust_maximum_thr` 0, so a frame near white keeps its level), clipped at white in camera space (`highlight` 0, so a clipped photosite stays neutral: unclipped, the matrix turns it magenta), where LibRaw divides by the smallest multiplier; the twin multiplies the smallest back, so the balance is anchored on green, as Core Image's, where dcraw alone brightens the frame as the temperature moves. Known difference: where a multiplier falls below green's (the slider's far ends), the frame's white falls to that multiplier. It makes the look itself, rows side by side: exposure multiplies linear light by 2^EV, clamped, then Boost mixes sRGB's curve (0) with LibRaw's BT.709 curve (1). Known difference: Apple's Boost curve is undisclosed; at 1 this is LibRaw's default curve. The result is opaque `RGBA8888_Premultiplied` tagged sRGB, oriented by LibRaw. Only three-colour sensors develop; a four-colour one is `unsupported` (dcraw's matrices hold no fourth to invert). A preview past its limit decodes at half size, then fits smoothly. `Queue` is Swift's actor: one develop at a time under a mutex, the unpacked frame kept for the previews of one file, a full develop always fresh, `release` letting go; Swift keys the cache by the limit too, since its filter's scale is fixed when made, where LibRaw halves per develop. LibRaw's refusals are `unreadable`, logged with its message on `omaphoto.io`; a surface that cannot be made is `ExportError(render)`. In the session (`EditorSession+Imports.cpp`) a RAW goes before the Photoshop check: `pixelSize` (unreadable when it fails) and Swift's budget (`tooLarge` past 30,000 a side or the pixels left), then `developRaw` shows the sheet with the camera's settings (the defaults when they cannot be read) and keeps the answer, `finishRawDevelop` clears the request, releases the queue on a worker and answers from the event loop (a resumed task): nothing skips the file, settings develop the whole frame on the import's worker, which catches `ImageImportError` and `ExportError`, into a layer named for the file. `rawDevelop` alone stands for Swift's `rawDevelop` and `showsRawDevelop`, which are set and cleared together; the latter's `didSet` is dead (the sheet shows only while importing, which holds file requests anyway), and so is the test hook `confirmRawDevelop`. `UI/RawDevelopSheet` is Swift's sheet in a window-modal `QDialog` (`rawDevelopSheet`) that `ContentView` shows while the request stands, its close button and Escape cancelling and a sheet the session takes away answering nothing: the title at 17 pixels bold, the 560 by 340 preview on 35% black rounded 6 with a spinner while a develop runs, four rows (a 90-point title as the buddy of a 300-point slider, the value 80 points wide in `PlaceholderText`: Exposure in hundredths, Temperature and Tint whole, Boost in thousandths, printed as Swift's `%.Nf`), Reset (resting while as shot), Cancel and the default Import. The first preview develops at once, each change after 60 ms (Swift's `task(id:)`), at 800 pixels through the queue on a worker; a newer change drops the older's result, and a failed preview keeps the last one, logged. Left out as dead: LibRaw's size check after identification (`adjust_sizes_info_only` fails only when called before), the second green's multiplier (LibRaw copies the first's), the lower clamp of unsigned samples, and a check of the output's colours and bits (three colours and 16 bits are asked for). Equivalent mutants: a response of exactly zero refused as below zero (a double lands on it only by chance), Import's explicit default (a dialog makes its first auto-default button the default) and the fit at exactly the limit (the scale keeps the size). Untested: a stale preview dropped while a newer one waits is ordered by the test through the wait timer alone. A dropped picture takes a RAW type after Photoshop's; the importer's filter and the desktop entry (`image/x-dcraw`) take RAW. Tests: `RawImporterTests` (matching, the locus and its round trips, the camera's balance, curves, exposure, colour, orientation, the preview's limit, the queue's cache), `RawDevelopTests` (the session, refusals and budgets, a develop that fails, the sheet), a tiny DNG writer in `DNGFixture.h`, the drop order in `ImageFileDropTests`, the filter in `ContentViewTests`, the entry in `DesktopIntegrationTests`. `tests/Tests.cmake` holds the test binaries, keeping `CMakeLists.txt` under 500 lines. - Middle-button pan, tool defaults, Auto Select and the icon (12.6b, Swift 1.2.1): `Document/ToolDefaults` twins Swift's enum as a namespace: `boolean` (Swift's `bool`, a C++ keyword) and `set` keep Auto Select, the transform controls, the pixel grid, the layout grid, guides, rulers, Snap, the four Snap To switches and Lock Guides in `QSettings` under Swift's `tool.` keys, read by the session's members as they are made and written by each setter, and by the two places that show guides by themselves (`beginGuideCreation`, `addGuide`), as Swift's `didSet` sees those writes. Swift returns the defaults under XCTest; Qt has no such flag, and most test binaries run outside `QStandardPaths`' test mode, so the twin keeps the defaults until `ToolDefaults::enable()`, which `main` calls first (not under test, as the theme's construction is not). The settings file keeps text, so a stored value reads as `true` or `false`; anything else is ignored with a warning on `omaphoto.app`, where Swift's `as? Bool` is silent. The canvas's middle button (Swift's `otherMouseDown` for button 2) pans from any tool while a document is open, on a drag point of its own (`m_middlePanPoint`), so a left drag under way goes on beside it: a move with both buttons pans, then gives the left its turn (so a Hand drag pans twice), and a right-button tip drag waits for the middle's release. Known difference: macOS sends one drag event a motion, so which button Swift's canvas follows then is AppKit's choice. The press hides the brush circle and shows the closed hand, which outranks every other cursor; the release puts the tool's cursor and circle back. Qt can lose a release: a move without the middle button ends the pan. Unlike Swift's `mouseDown`, the press takes no focus. `transformPressLayer` gains Swift's foreground rule: with Auto Select, a press inside the active layer's box goes to the layer under the pointer when that is painted above it (a background holding every press no longer hides the layer on top); `above` reads both layers through checked lookups, since the pointer's layer and the visible active one are both drawn, where Swift guards each index. Swift's `underPointer != active.id` is left out as dead (`above` refuses a layer against itself); equivalent mutant: `>=` in `above` (only the active layer equals itself, and `selectLayer` of the active layer changes nothing). The reshaped app icon (`45ab8fd`) replaces `packaging/icons`. Tests: `ToolDefaultsTests` (the defaults until enabled, every toggle across sessions and as text, guides shown by a guide, a strange value), `theMiddleButtonPansFromAnyTool` in `CanvasNavigationTests`, Swift's `autoSelectPicksForegroundLayerStackedOnSelectedBackground` and a mask box past its layer in `TransformPressTests`, whose older Auto Select test now expects the layer above. -- Photoshop's blend modes (12.7a, Swift 1.2.2): `LayerBlendMode` takes Swift's order, Photoshop's, and `allLayerBlendModes` follows it, so the blend keys step Normal, Darken, Multiply…; the raw values are Swift's (`Linear Dodge (Add)` among them), so the manifest reads them without a new version, as Swift's. `blendGroup` stands for Swift's static `groups`: each group's modes sit together in the enum, so the group is the last first member a mode reaches. `BlendModePicker` draws a separator between groups, as Swift's pop-up does; QComboBox's keys step over them and never make one current, so no preview names one. Hard Light and Exclusion are QPainter's composition modes; the other eight go by hand (`SeparableBlend`, `HslBlend`). The PSD reader takes Swift's new keys (`lbrn`, `lddg`, `hLit`, `vLit`, `lLit`, `pLit`, `hMix`, `smud`, `fsub`, `fdiv`); Dissolve, Darker Color and Lighter Color fall to Normal with the conversion note, and Swift's renamed test uses Dissolve. A fix beyond Swift: Swift's `LiveMaskRenderer` composites a clipping stack's group and an adjustment's result through `cgMode`, which is Normal for the eight (v1.2.2's `prepareStacks`, `drawComposite` and `adjust`, unchanged through 1.3.3), so a base or an adjustment in one of them draws as Normal there; the twin hands every mode to `LayerRenderer::composite`, which blends the eight by hand, as Photoshop composites a clipping group with its base's mode (`ClippingStackTests::aStackBlendsOnceWithItsBasesMode`, `AdjustmentLayerTests::adjustmentBlendAndSoftMaskPreserveCoverage`). Swift's `coreImageFilter` has no twin: `needsSurface` names the modes. Equivalent mutants: `<=` against `<` at one half in Vivid and Pin Light (both branches give the backdrop there). Tests: the formulas in `BlendModeTests`, the menu's groups and keys in `BlendModePickerTests`, the order in `LayerAppearanceTests`, `BlendShortcutTests`, `EditorCanvasTests` and `ProjectManifestTests`, the keys in `PSDRoundTripTests` and `PSDImportTests`, the canvas against the export in `CanvasDisplayTests::photoshopsOwnModesShowAsTheExportDoes`. +- Photoshop's blend modes (12.7a, Swift 1.2.2): `LayerBlendMode` takes Swift's order, Photoshop's, and `allLayerBlendModes` follows it, so the blend keys step Normal, Darken, Multiply…; the raw values are Swift's (`Linear Dodge (Add)` among them), so the manifest reads them without a new version, as Swift's. `blendGroup` stands for Swift's static `groups`: each group's modes sit together in the enum, so the group is the last first member a mode reaches. `BlendModePicker` draws a separator between groups, as Swift's pop-up does; QComboBox's keys step over them and never make one current, so no preview names one. Hard Light and Exclusion are QPainter's composition modes; the other eight go by hand (`SeparableBlend`, `HslBlend`). The PSD reader takes Swift's new keys (`lbrn`, `lddg`, `hLit`, `vLit`, `lLit`, `pLit`, `hMix`, `smud`, `fsub`, `fdiv`); Dissolve, Darker Color and Lighter Color fall to Normal with the conversion note, and Swift's renamed test uses Dissolve. A fix beyond Swift: Swift's `LiveMaskRenderer` composites a clipping stack's group and an adjustment's result through `cgMode`, which is Normal for the eight (v1.2.2's `prepareStacks`, `drawComposite` and `adjust`, through 1.3.3; Swift 1.4 blends both in every mode, 13.9a), so a base or an adjustment in one of them drew as Normal there; the twin hands every mode to `LayerRenderer::composite`, which blends the eight by hand, as Photoshop composites a clipping group with its base's mode (`ClippingStackTests::aStackBlendsOnceWithItsBasesMode`, `AdjustmentLayerTests::adjustmentBlendAndSoftMaskPreserveCoverage`). Swift's `coreImageFilter` has no twin: `needsSurface` names the modes. Equivalent mutants: `<=` against `<` at one half in Vivid and Pin Light (both branches give the backdrop there). Tests: the formulas in `BlendModeTests`, the menu's groups and keys in `BlendModePickerTests`, the order in `LayerAppearanceTests`, `BlendShortcutTests`, `EditorCanvasTests` and `ProjectManifestTests`, the keys in `PSDRoundTripTests` and `PSDImportTests`, the canvas against the export in `CanvasDisplayTests::photoshopsOwnModesShowAsTheExportDoes`. - Black & White, Color Balance and Invert (12.7b, Swift 1.2.2): `kernels/AdjustPixels.c/.h` are v1.2.2's, with `adjust_black_white` and `adjust_color_balance`, run through `ImageAdjustmentPixels::run` on one thread as Swift's. `BlackWhiteSettings` and `ColorBalanceSettings` keep Swift's fields, defaults and bounds (the `isFinite` terms left out, as the bounds refuse NaN and infinities); `apply` throws `ProjectError(invalid)` past them and Color Balance hands an untouched image back. Swift's `FilterSettings.normalized` does not clamp either, so a number typed past a slider's range fails the preview with that error and a commit sets `brushError` with no step, as in Swift: the filter's two workers catch `ProjectError` beside their other errors, where it would otherwise escape as `QUnhandledException` and abort. `FilterKind` gains both after Grain (`isImageAdjustment`), `AdjustmentKind` Invert, Black & White and Color Balance in Swift's order, with `isEditable` (all but Invert) and `filterKind`; `LayerAdjustment` gains the two optionals with Swift's computed getter and setter pairs, validity and `apply` (Invert through `PixelInvert::run`, whole image, colour only); the manifest's `blackWhiteSettings` and `colorBalanceSettings` are Swift's synthesized `Codable`, every key required, left out while absent. `addAdjustment` opens the editor for editable kinds alone: an Invert layer applies at once. The editors carry both settings through `beginAdjustmentEditing` and `editedAdjustment`. A fix beyond Swift: Swift's Edit Adjustment… stays enabled on an Invert layer, whose editor switch breaks after the original is kept and the transaction opened, so nothing ever closes them and the session stays refused; the twin's entry rests for a kind that is not editable, and the two switches throw a `logic_error` for Invert, which nothing reaches. As Swift's row, an Invert layer says "Adjustment · Double-click to edit", yet a double click on its glyph renames it (`m_editableAdjustment`, Swift's `isEditable` test). The Image menu lists Black & White… and Color Balance… before Exposure…, the Layer menu's New Adjustment Layer entries take an ellipsis when editable, and a menu doubles the ampersand of "Black & White" (a lone one is a mnemonic in Qt), in the kinds' entries and in Undo and Redo's step names alike; object names drop it (`blackWhite`, `newBlackWhiteAdjustment`). `LayerIcons` draws Swift's three new symbols as paths: `circle.righthalf.filled`, `circle.filled.pattern.diagonalline.rectangle` (a rounded frame, diagonal lines, a filled circle) and `scale.3d` (three axes with arrowheads). The filter sheet's two kinds live in `UI/FilterSheet+Adjustments.cpp` (the sheet's file neared 500 lines): the six families and Tint's Hue and Saturation, shown while Tint is on (Swift's `if`, the rows `m_shownWhen` holds with Advanced's), and Color Balance's three headlines (13 pixels, semibold, Swift's `.headline`, as the effects sheet's) over three rows each and Preserve Luminosity; each `flag` is Swift's `Toggle` with its help as the tooltip. `NativeLayerList` stops listening in its destructor, as `ContentView` does: closing a list window hides it first, and a rename's focus-out then commits into a list half destroyed. Tests: Swift's `invertAppliesWithoutAnEditor` in `AdjustmentLayerTests`, its editor test over the editable kinds in `AdjustmentEditorTests` and `FloatingPanelTests`, `ColorAdjustmentTests` (the kernels against Photoshop's defaults and a tint worked in HSL, the tones of Color Balance and Preserve Luminosity, bounds, every manifest key, the sheet), the kinds, keys and menus in `FilterModelTests`, `LayerAdjustmentTests`, `AdjustmentPanelTests`, `CompositorMenusTests` and `SelectMenuTests`. `ProjectExportFailureTests`' two panel limits rose from 2,048 KB: the thousand-layer snapshot grew by the two optionals of every record (2,560 KB since 12.8d). - The Camera Raw filter's model (12.8a, Swift 1.2.3): `kernels/AdjustPixels.c/.h` are v1.2.3's, with the Camera Raw kernels and Grain's particle size (`59b5040`); that kernel warns under `-Wunused-parameter` and `-Wmisleading-indentation` through 1.3.3 (both lines date from `9982fad`; `075a71b` fixes another), so CMake silences those two for that file alone. `Document/CameraRaw.h/.cpp`, `CameraRawColor`, `CameraRawDetailOptics` and `CameraRawGeometryCalibration` twin Swift's four files; Swift's `CameraRaw.swift` runs to 638 lines, so its session extension is `CameraRaw+Session.cpp` with `CameraRaw+Session.h`. The enums keep Swift's raw values and order; `CameraRawClipping` keeps Swift's bits (highlights 1, shadows 2); `Balance` and `Gains` are structs for Swift's tuples; `region` returns a reference where Swift writes through a key path. `CameraRawDrag` sits in `CameraRaw.h`, where Swift's sits in `CameraRawColor.swift`: it holds the whole settings, which C++ needs complete. Swift's panel state sits on `FilterEdit` (the shown groups, pages, swatch, point index, the tools, the guide draft, clipping, sharpen mask, the scope and its mode, the readout); the twin gathers it in `CameraRawPanel`, `FilterEdit::rawPanel`, written through `setCameraRawPanel`, which notifies while an edit is open. `renderSettings` is Swift's settings with the hidden groups left out (`applying`); the commit renders and keeps those, so a hidden group stays out next time, and an identity grade closes as Cancel does. `previewJob` carries the panel's views; the preview's worker runs `CameraRawScope::preview`, whose scope lands on the panel. A fix beyond Swift: Swift's `preview` drops the point colour's view (it runs the job itself only for clipping or the sharpen mask, through 1.3.3), so Visualize never shows; the twin runs the job for it too. Geometry's warp is `QTransform::quadToQuad` from the image's rect to Swift's corners flipped to y down, drawn smooth and antialiased; a warp Qt cannot solve is a `logic_error`. Constrain Crop fits the frame inside `brush_alpha_bounds`; a crop whose copy finds no memory is `ExportError(render)`, where Swift's `cropping` returning nil falls back to the uncropped warp. Left out as dead: `apply`'s `isValid` guard and with it `isValid` (settings arrive normalized, and nothing else asks), `nudged`'s guard for an empty channel (`normalized` repairs every curve to two points; `at` throws if that breaks), the vectorscope's wrap of a negative hue (its angle's cosine and sine turn round alike), the parametric curve's `max(0, weight)` (a tone inside its region weighs 0 to 1), each group's `applying(shows:)` (no caller), and the defringe eyedropper's own `normalized` (`updateFilter` normalizes). Equivalent mutants: `tone < shadow` against `<=` (the regions meet with equal weight), the defringe tie's `<` against `<=`, the mixer's `weights > 0` against `>= 0` (a zero weight adds nothing), the identity commit's cancel (the history drops an unchanged step), and Auto's `committing` term (a committing edit refuses `updateFilter`, and the landing asks again). The Filter menu and the sheet leave Camera Raw out until its panel (12.8b). Tests: `CameraRawTests`, `CameraRawSessionTests` (Swift's session tests; the preview's views and scope), `CameraRawToolsTests`, `CameraRawRulesTests`, `CameraRawColorTests`, `CameraRawLensTests` (the kernel's calibration called directly), `CameraRawWiringTests` (Effects, Optics and Detail against their kernels called with the header's order, a clip view on an untouched grade, Auto in linear light, every eye's fields, Offset, Aspect and Constrain Crop against placed pictures, the scope's channels and hues, the curve, mixer and grading call with its floats written out, the sharpen-mask view, each lens flag alone, Grain's scale, a dull colour's reach, Rotate, Scale and Vertical against placed pictures), `CameraRawFixtures.h`, Grain's size in `ImageAdjustmentTests`, the kind in `FilterModelTests`. - The Camera Raw panel (12.8b1, Swift 1.2.3): Swift docks the filter's `NSPanel` to the document window's right edge, full height, following it (`dockedToMainWindowRight`). Wayland lets no client place a window, and a tiling compositor would tile a second one, so the twin docks inside the window: `FloatingPanel::show` takes a dock slot, `ContentView`'s `panelDock` at the right of the middle row, `FloatingPanel::dockedWidth` (Swift's 440) wide, and there the panel is its own `PanelWindow` made a plain child (`Qt::Widget`), so `QDialog` still takes Escape as Cancel and Return as the default button; a header holds the title and a close button, which cancels. A docked frame is never remembered (`moveEvent` asks `isWindow`), so Gaussian Blur and the rest reopen where they were left, Swift's `dockedPlacementLeavesTheSavedFilterPosition`; `refocus` hands a docked panel the keys through `setFocus`, looking past the hidden floating panel of the same name, which is the window's child too. The dock takes room beside the layers panel and spans the middle row alone, so the tool bar and status bar keep their width, where Swift's panel lies over the window's whole height (known difference). Refocus hands the keys to the docked panel itself, not the field last typed in (known difference: `makeKey` keeps its first responder). `UI/CameraRawSlider` twins Swift's file: `CameraRawSliderTrack` with Swift's colours, the gradient drawn 4 points high across the groove under the style's knob, a double click on the knob (its rect two points out) resetting; a press on the track is `SliderSnap`'s (10.5): Swift's 0.18 s glide has no twin, and a drag goes on from the snapped knob, as every slider of the twin does (known differences); `display` keeps a knob under a drag, Swift's `isTrackingValue`; Camera Raw's own rows keep the track they are made with, since the rows whose tracks change (12.8b2's mixer) are made anew; since 12.14c `reshape` is Swift's track and bounds update in `updateNSView`, for the filter and Hue/Saturation sheets. `UI/CameraRawControls.h/.cpp` and `CameraRawControls+Basic.cpp` (Light, Color, Effects) twin Swift's view: the scope 110 high on black at 35%, the three ribbons at 55% in SwiftUI's dark red, green and blue (the Levels histogram's values) or the vectorscope's cells, square by the width and counted up from the bottom, so a 110-point graph shows its lowest 18 rows, as Swift's does, its context menu choosing the mode, the two clipping triangles lit in that blue and red, the readout; the words say Alt for Option and a right click for Control-click; the ten groups, Light and Color open, each eye shown while its group adjusts; a row's title (96 points at least, a double click resetting), `CameraRawSlider` and a 56-point `PickerField` showing Swift's `.number` to the row's decimals. A row's object name is its settings field (`vignetteAmountSlider`), since titles repeat. Alt is read from `QGuiApplication::keyboardModifiers`, Swift's `NSEvent.modifierFlags`, and its release (auto-repeat aside), from any window, ends the clipping and sharpen-mask views (Swift's local monitor). Curve to Calibration fill their groups in 12.8b2, and the Filter menu's entry waits for them. On the canvas (`EditorCanvas+CameraRaw.cpp`) a hover reads the readout (Swift's `mouseMoved`, never a drag), leaving clears it, the three eyedroppers sample and hand the panel the keys, the guide and the targeted drag begin, go on and end on release, and a move without the left button ends either, since Qt can lose a release; the four tools are `picking`, so the eyedropper shows. `EyedropperIcon::icon` came from `LevelsSheet`, which shares it. Left out as dead: the canvas's `kind == cameraRaw` and edit terms before the readout (the session asks), a hide of the other placement's panel in `show` (every edit closes its panel first), Swift's clipping reset before a row's reset (`assign` clears it), and the docked panel's own hide on close (its slot hides it). Equivalent mutants: the titles' minimum width, 96 points (a layout sizes labels offscreen alike), and the unlit triangle's 55% white (cosmetic, over whatever the scope shows). Two consequences of the named differences: a double click on a slider's track resets it, since the first press puts the knob under the second; and a canvas press takes the keys, so a field still being typed in commits at the click, where Swift's panel keeps its field editor. Tests: `CameraRawSliderTests` (Swift's three), `CameraRawPanelTests`, `CameraRawCanvasTests` (the dock in a real window among them), `FloatingPanelTests::dockedPlacementLeavesTheSavedFilterPosition`. @@ -168,8 +168,8 @@ Platform adaptations, each forced by a missing Qt analog: - Tabs reorder and overflow (13.14, Swift `1faf7a0`): `UI/ProjectTabLayout.h/.cpp` twin Swift's file: `projectTabSpacing`, `ProjectTabSlot`, `ProjectTabPillSlot`, `ProjectTabOverflow` (`contentWidth()` for Swift's computed property), `projectTabOverflowLabel` and `projectTabOverflow`, widths a `std::map` read through `at` (every tab has one), the pill's width a `std::function`. Left out as dead: `span`'s empty guard (never asked of no tabs) and `contentWidth`'s pill fallback (a pill always has a tab after it). `ProjectWorkspace::moveTab` is Swift's, clamped, and emits `changed()` only when a tab moved. `ProjectTabStrip` is now a plain `QWidget` that takes the toolbar's free width (the spacer after it went; its size hint is every tab in a row, its minimum none) and places its children by hand from `projectTabOverflow` over its own width: the overflow pill at x 0 and the visible tabs at y 3, 28 high, each as wide as its button's size hint (so the unsaved dot's extra width counts, as before), hidden tabs hidden; it lays out again on `resizeEvent`, on the workspace's signal and on each tab's session signal (a title or a dot changes widths; a hidden button posts no layout request). The QScrollArea, its offset and `showFront` are gone, as Swift's scroller and reveal are; the selected tab is always shown by the pinning. `OverflowTabsPill` (a `QToolButton`, `projectTabsOverflow`) paints an inactive tab's capsule, the label "N more tabs" at 12 pixels medium and a drawn `chevron.down`, the label and chevron at half opacity while the workspace cannot switch (the capsule stays, as Swift's `.opacity` sits before its background); tooltip and accessible name are the label; its width is Swift's `projectTabOverflowPillWidth` (the label's ceiling plus 11, 4, 10, 11). Its press opens a `QMenu` (`projectTabsOverflowMenu`, deleted on close) four points below it, as `popUp(positioning:)` does, listing the hidden tabs in order, "• " before an unsaved one, a choice selecting; while the workspace cannot switch no menu opens. The reorder twins Swift's: `ProjectTabButton` watches its title button's left press, move and release in global coordinates (Swift's `.global` space) and reports `TabDragPhase::changed` once the press has moved three points and `ended` on the release; the strip's `Reorder` is `TabReorderState` (others, widths, compacted x, start, origin, translation, target), `nearestSlot`, `renderX`, `makeReorder` and `commitReorder` as Swift's (a neighbour closed mid-drag has no place, as Swift's `if let`s). A drag begins only while the workspace can switch and across at least three points, selects its tab first and raises it over the others (Swift's `zIndex`); it commits on release only if the workspace can still switch; the others ease to their places through a 150 ms `QPropertyAnimation` of `pos` (`OutQuad` for SwiftUI's `easeOut`), the dragged tab following the pointer at once, and every tab eases home when it ends. Qt can lose a release: a drag whose tab closes is dropped on the workspace's signal (Swift's timer drops it too), and a drag on another tab drops a stale one first (Swift's timer would have cleared it within 0.1 s). Known difference: a lost release on the same tab leaves its stale state for that tab's next drag, which then measures from the old origin. Known difference: Swift's empty title bar beside the tabs drags the window (`TitleBarDragView`); Qt's toolbar is no title bar, so the strip's empty part does nothing. `NewTabDropSlot` has no twin, as before. Equivalent mutant: reporting `ended` after a click that never dragged (Swift's gesture never ends one); it differs only after a lost release, where it would drop another tab's stale drag a click sooner. Tests: `ProjectTabLayoutTests` (Swift's six, the bounds at exactly fitting, a lone wide pinned tab, the pill's width asked by count), `ProjectWorkspaceTests::moveTabReordersWithoutTouchingSelectionOrDocuments` (Swift's, plus its signal), `ProjectTabDragTests` (following the pointer, clamping, the gap, the slide's duration, curve and end, stacking, landing left and right and in place, a small or upward move as a click, a busy project at the press and at the drop, a tab closed mid-drag, a lost release giving way to the next drag, a three-point move and a move under another button, the overflow pill's label, place and width, the menu's entries, place and choice, pinning, the busy pill), and `ProjectWorkspaceViewTests` (overflow in the window; the strip ends before the zoom buttons). `ProjectTabStripTests::onlyANewFrontTabScrollsIntoView` went with the scrolling. - Ungroup Layers and the mask pointer (13.15, Swift `1318f1e`, `6c3b9a5`, `6955a6f`): `EditorSession::canUngroupLayers` and `ungroupLayers` sit in `LayerGroups.cpp` (declared in `LayerGroups+Session.h`), as Swift's extension: the folder's direct children, their parent now the folder's own, take its place in the list in their order; `releaseDetachedClipping` then frees clips that no longer sit on their base; `finishOpacityEdit` ends a drag first; one `Ungroup Layers` step selects the children with the lowest active (none for an empty folder) and forgets the folder's collapse. Swift's `LayerHierarchy.validate` guard is dead and left out: unwrapping a folder only shortens parent chains, so nothing it refuses can appear; its `document` guard is `canEditLayers`'. The Layer menu gains Ungroup Layers (Ctrl+Shift+G) after Group Selected Layers, enabled by `canUngroupLayers`, and `ShortcutDefinition::all` Swift's "Ungroup Layers" entry (g, Cmd+Shift), so it can be remapped; the list's context menu shows it after Group Selected Layers for a right-clicked folder alone (Swift reads the clicked row, not the active layer), enabled by `canUngroupLayers`. `NativeLayerList::showMaskCursor` twins Swift's `showMaskCursor`: the duplicate pointer drawn over an eye in a 7.5 by 5.5 box at the hot spot plus (15, 18), haloed by sixteen white copies one point out, the image widened to hold it (29 by 32 at 1×), the hot spot the pointer's; Swift's `eye.fill` is drawn as paths (an almond of two quadratic curves, a ring cut round a filled pupil). Over a mask thumbnail with Alt, `cursorFor` shows it whatever `canEditLayers` says, as Swift's now does (13.12 brings the Alt-click that shows the mask alone). Equivalent mutant: the context entry enabled by `canEditLayers` (the right-click makes the folder active, so `canUngroupLayers` says the same). Tests: `GroupSessionTests` (Swift's three ungroup tests; collapse forgotten, signals, a plain layer refused silently, a nested and an empty folder, an opacity drag ended, busy), `LayerContextMenuTests::aFoldersMenuOffersUngroup`, the menu entry and its gate in `CompositorMenusTests`, the shortcut count in `KeyboardShortcutsTests`, the eye pointer's pixels and hot spot in `LayerDragGestureTests`. - Compositor 1.4, part one (13.8: `8c0417b`, `fe7a83d`, `af3b568`, `3ccc125`, `d0135c8`, `dcf1c66`). A sharper canvas: `LayerRenderer::interpolation` takes Swift's `upright` (a layer whose `radians()` is 0, drawn within a thousandth of pixel for pixel, takes no filter), from `LayerRenderer::draw` and `TiledLayerRenderer`'s frames alike. The twin's adjustment surfaces already held device pixels (9.5b) and its adjustments already ran at the surface's scale (12.8c), so 1.4's surface and `adjustmentScale` changes were held; what was missing is ported: `AdjustmentSurface::draw` falls back to a pixel a painter unit when device pixels pass the budget (Swift's "falls back to points"), drawn back smoothly (Swift's `.high`), and nothing only when that passes too; `LayerAdjustment::apply` counts the noise origin per axis (`top × height / max(1, region height)`), where the twin used the width's scale for both. PSD (`IO/PSD`): Levels gamma in hundredths; `hue2` read as Swift's: 16 bytes at least, Colorize's own values or the Master's, then for each of the six ranges its band (degrees wrapped into 0–360) and values, 14 bytes each, until the data runs out; the reader keeps each mask's bounds and default byte (`PSDRecord::maskBounds`, `maskDefault`), and `PSDDocumentBuilder::maskOnLayerGrid` draws the stored patch, unfiltered, onto the layer's grid (its pixels, or the canvas for a layer without) at its place, the default round it; already the grid, or with no size, the patch is kept; a grid that cannot be allocated logs on `omaphoto.io` and takes the builder's existing "couldn't be converted" note, as Swift's nil does. `PSDAdjustments::levels` and `hue` are public, as Swift made them for its tests. Photoshop's Saturation: `HueSaturationFilter::adjustedSaturation`, Swift's to the letter. All cores: `kernels/LevelsPixels.c/.h` are Swift's file again (`cube_apply`); `BrushRaster::copy` (bytewise for the context's own format, drawn otherwise) and `BrushRaster::inBands` (one band under 250,000 pixels, else twice the ideal thread count, through `PoolMap`; a `bad_alloc` there is `ExportError(render)`, since the workers' callers catch that alone); `LevelsFilter::run` copies and runs `levels_apply` in bands; `HueSaturationFilter::run` copies and runs `cube_apply` in bands, where the twin had its own trilinear loop (the kernel's float order now: within a level of it, Swift's tests allow eight); `cube` keeps the last eight tables under a mutex, Swift's `cubes`. A blur's preview kept while the layer grows: `FilterEdit` gains `preparedTransform` and `pendingTransform`; a render carries the grown layer it was made from (`Filtered::placement`) and lands there, shown until the next replaces it; `displayedTransform` and `displayedMaskPlacement` read `preparedTransform`. Left out as dead in the twin: Swift's `current` terms (only a blur grows its grid, and only the automatic kinds, which never grow, refuse a stale preview or read `preparedSettings`), so the grid's version (`previewSourceVersion`, `Filtered::sourceVersion`) goes with nothing left to read it, and so does the twin's own remaking of a waiting job on growth (with Preview off nothing lands; with it on, the update makes a new job). The hierarchy kept: `LayerOrder` (`Document/LayerGroups.h`, its code in `LayerGroups+Order.cpp` with the document's hierarchy members, since `LayerGroups.cpp` would pass 500 lines): `Node`, `Result` (`order`, `visible`, `drawn`) and `resolve`, which keeps the last nodes and answer under a mutex; `CanvasDocument::hierarchy()` replaces `hierarchyEntries()`; `effectiveVisibleIDs`, `renderLayers` (a checked `std::map` index, after Swift's empty-order guard) and `effectiveOpacities` (a map of opacity and folder, no whole layers) read it; `groupSelectedLayers` reads `hierarchy().order`. `dcf1c66` was held: the canvas's `DisplayState` already works out the visible set once. Tests: `SharpCanvasTests` (the rule's edges; plain, masked and tiled layers crisp within the tolerance, filtered past it), `PSDAdjustmentTests` (Swift's three, plus the wrap, Colorize leaving the ranges, a cut record, the builder, a pixel layer's grid, a stretched layer, the patch kept, bounds or a layer of no height or width, the grid's bounds with a smaller patch, a patch the grid's size a pixel along), `PSDReaderTests` (the new layouts, the mask's bounds and default read and placed), `HueSaturationTests::positiveSaturationMatchesPhotoshop` (Swift's values, the clamps, a pixel), `AdjustmentBandsTests` (bands cover each pixel once, the copy's bytes, Levels and Hue/Saturation in bands against one pass of the kernels, cached tables), `HueSaturationFailureTests` (the filter now fails at its copy, the row list being gone), `FilterTests::growingABlurKeepsThePreviewUpUntilTheNextOne` (Swift's) and `aMaskKeepsItsPlaceUnderAPreview` (a covering mask under a preview that did not grow, and on a grown one the old bounds), `FilterSessionTests` and `FinishingFilterTests` (an older grid's preview shows on that grid: `aPreviewForAnOlderGridShowsOnThatGrid`, `aJobWaitingBehindAnotherEditShowsOnItsGrid`), `LayerOrderTests` (order, visibility and drawing against `LayerHierarchy::entries`, each shaping change seen, 64 deep, nothing shown draws nothing, opacities up known folders alone and a repeated id refused), `GroupTests`, `BlurNoiseAdjustmentTests::noiseStaysOneFieldAcrossRegions` (each axis by its own scale, a region under a unit), `AdjustmentSurfaceTests::pastItsPixelBudgetNothingIsDrawn` (the fallback, enlarged smoothly; a quarter of a pixel makes one). Equivalent mutants: `adjustedSaturation`'s clamps of the amount to ±1, its `max(0, …)` below zero and `share > 0` for `>= 0` (each branch gives the same number there; Swift's to the letter); the cube cache's insert, its removal of an older entry for the same settings and its trim to eight (each only spares work or memory); `BrushRaster::copy`'s bytewise branch taken never (drawing copies the same bytes), `inBands`' threshold moved by one and its `start < count` guard (Swift's; no start reaches the count unless the thread count passes some 500); `LevelsFilter::run` drawing its copy (the same bytes); `LayerOrder::resolve` keeping nothing (the cache only spares work). Untested failure path: `maskOnLayerGrid` out of memory (only an allocation that fails reaches it). Judgments: the adjustment surfaces' device resolution and `adjustmentScale` were held (9.5b, 12.8c); `dcf1c66` was held (the canvas already works out the visible set once); the rest ported. Mutants: 60 run, every one caught or recorded above (two unbuildable forms rewritten and caught). -- Blending (13.9a: `6dc2219`, `8ab32c9`, `5a8f6ce`, `e6c964b`). Held, proven: clipping stacks and live-mask coverage at device resolution (the twin's `LiveMaskRenderer` has no `bounds`: its group, alpha and coverage surfaces are the painter's device since 4.x, so Swift's `resolution` and `pixelSize` have no twin); an adjustment in any mode, over the document and inside a stack, blended at full coverage, its alpha restored, then mixed through mask and opacity (`adjust`'s `blended` goes through `LayerRenderer::composite`, which hand-blends every mode QPainter lacks through `HslBlend`, so Swift's `SeparableBlend.blend` has no twin); a clipping stack composited in its base's mode in every mode (`drawComposite` hands `m_stackModes` to `LayerRenderer::composite`, the same path), on the canvas, in export and in Copy Merged alike. Ported: Soft Light by Photoshop's formula, as Core Image computes it (2bs + b²(1 − 2s) up to half, 2b(1 − s) + √b(2s − 1) above), in `SeparableBlend::channel`, and `needsSurface` names Soft Light, so `HslBlend` blends it by hand everywhere a layer, a stack or an adjustment is drawn; `LayerRenderer::compositionMode` loses its Soft Light case (dead now). Compared first: Qt's `CompositionMode_SoftLight` is the PDF's (W3C) formula, which equals Photoshop's below a source of half and above it wherever the backdrop passes 0.25; under that it takes the PDF's cubic for √b and runs up to 17 levels darker (backdrop 5/255, source 1: 19 where Photoshop gives 36). Swift's 0.9 over 0.5 gives 170 under either formula. Known difference: Swift's Core Image is "within 5 levels of Photoshop's"; the twin is Photoshop's own formula, so it may differ from Swift's by those few levels. Tests: `BlendingTests` (Swift's `clippingStacksBlendInTheirBasesMode` for Linear Dodge, Color Dodge and Multiply: Swift's numbers, Copy Merged equal to export, the canvas within a level; adjustments in Difference and Linear Light over the document and Vivid Light and Subtract inside a stack, by the numbers, at full and half opacity, in all three places; Swift's `softLightMatchesPhotoshop` plus a dark backdrop and the lower branch; Swift's `softEdgedLayersBlendTheSame` for Soft Light, Overlay, Multiply and Color Dodge: a radial soft layer over noise, Copy Merged equal to export, the canvas within a level), `BlendModeTests::everyBlendModeMatchesThePdfFormulas` (its Soft Light reference is Photoshop's now; its backdrops of 0.1 and 0.25 under sources past half catch the PDF's cubic), `ClippingStackTests::stacksAndCoverageTakeTheDevicesPixels` (a half-unit stripe stays sharp at a device scale of two, in a stack and through coverage from a source further down). Equivalent mutant: Soft Light's `source <= 0.5` for `< 0.5` (both branches give the backdrop at a half). Judgments: stacks and coverage at device resolution, adjustments in any mode and a stack in its base's mode were held (proven by the tests above); Soft Light ported. Mutants: 5 on Soft Light, 4 caught, one equivalent (above). -- The Metal canvas (13.9b: `cdf8674`, `4b04323`, `5708ed3`, `93de9e7`, `54dd707`, `91cb0b1`, `f773f7b`, `4c5e1a6`, `d04f159`, `b1b7490`). OmaPhoto stays a CPU canvas (the port's decision): Swift's GPU canvas, its `GPUCanvas`, `GPUNoise`, `MetalWarp` and `CanvasLinesOverlay` and the GPU branches of `EditorCanvas`, have no twin; the twin's canvas is the Core Graphics canvas those commits compare against. Ported: Grain and Add Noise anchored to the document (`54dd707`): `LiveMaskRenderer::adjustmentRegion` (Swift's `adjustmentRegion`) maps what the painter shows into the document, and the canvas's `drawLayers` sets it from its centre mapping and zoom, so the smooth path counts noise in frame pixels from the document's corner and Grain in document pixels, as export does, and the crisp path's surface maps to document pixels, so a partial repaint keeps one field; the blur's scale still comes from the unmapped region. This retires 9.5b's and 12.8c's known differences (Grain and noise following the view, a partial repaint's own field); `CanvasDisplayTests::grainFollowsTheViewOnTheCanvas` is gone. Export and `drawLiveComposite` already draw in document pixels and set no region, as Swift's. Tests: `AdjustmentCanvasTests` (Swift's `matchesEveryAdjustment`: every kind through a ramp mask at 80% over a photo and a translucent layer, the canvas within a level of the export over its checkerboard; Swift's `patternsStayWithTheDocument` for Add Noise and Grain: equal to export at 100% and 200%, panned and not, and at 80% a whole-pixel pan moving the pattern with the image and a partial repaint showing the whole's pattern within two levels of resampling), `CanvasEditContractTests` (Swift's `aNewMaskRevealsTheWholeLayer`, upright and turned: a one-pixel mask on a scaled layer changes no covered pixel, the canvas as the export; `matchesWhileMovingPixels`, moving and duplicating on a masked layer at 60% under a soft selection: the preview equals the commit and the export outside the ants, the hole only for a move; `matchesWhileDraggingAGradient` in a selection at 70%: the pending gradient equals its commit outside the overlays, nothing outside the selection; `5708ed3`'s failed fill cancelling with `brushError`, no step, the document unchanged; `matchesALiveMaskOutsideAClippingStack`: a turned source with a layer between, the canvas as the export), `CanvasContractFixtures.h` (the scene, the export over the canvas's checkerboard, a gap that skips the overlays' bands, the ramp mask). `cdf8674`: rotation, flips, scale, sharp successive reductions, premultiplied blending in every mode with layer and ancestor opacity, the checkerboard anchored to the document, the crisp composite at high zoom, sampling modes and rounded-up reduction extents, premultiplied blending in sRGB, the surround, shadow and border, placed own masks and multiplied folder masks on pixel layers, filter, Levels and Hue/Saturation previews in the layer's place: held; the CPU paths are the reference and are pinned by `LayerRendererTests` (placement, sampling rules, halvings, overhang), `DownsampleTests`, `BlendModeTests`, `CanvasDisplayTests` (checkerboard, shadow and border, crisp from 200%, the rich scene against the export), `FolderOpacityTests`, `FolderMaskClipTests`, `MaskPlacementTests`, `LevelsCanvasTests`, `HueSaturationCanvasTests`, `VignetteCanvasTests`. `4b04323`: a moved or duplicated selection's preview and commit: held and now proven on a masked, translucent layer (above). Swift's laziness (`PixelMove.applyOffset`, `holed`, `liftedTarget`) only spares the GPU canvas a rebuild per move; the twin rebuilds eagerly in `movePixels`, which realises the tiles before display and commit by construction, and a failure there cancels the move with `brushError` (`PixelMoveTests::aPixelMoveHoldsTheSessionAndAFailedMoveCancels`). `5708ed3`, `91cb0b1`: live paint, erase and mask paint, paint past the old bounds under the old mask (`TiledStrokeTests`, `BrushPreviewTests`, `MaskPaintAnywhereTests`), Smudge and Liquify previews through the mask, opacity and placement (`SmearCanvasTests`, `FolderOpacityTests`): held. `GradientEdit.Fill` and `applyFill` are the GPU's laziness; the twin fills in `refreshGradient`, before display and commit by construction, a failure cancelling with `brushError` (now tested), and the commit publishes what was filled. `MetalWarpTests.matchesTheCPU`: the twin's `WarpStroke` is the CPU reference (`SmudgeLiquifyTests`); nothing to port. `93de9e7`: a one-pixel white mask revealing its layer: held (QPainter's smooth draw clamps an image's edge, as Core Graphics draws it; now tested); the live paint grid's extent and edge sampling: held (`TiledStrokeTests`, `DownsampleTests`, `TiledLayerTests`). `clampedToExtent` is GPU-only. `54dd707`: every adjustment kind with alpha, masks and opacity, blurs at the drawn scale and direction, adjustment results equal to the CPU reference, deterministic seeds: held (the canvas runs the CPU filters themselves; now tested for every kind); Grain and Add Noise anchoring: ported (above). `f773f7b`: non-adjacent live-mask sources (now tested), distortion previews (`DistortCanvasTests`, `DistortMaskTests`, `EffectsCanvasTests`), text previews keeping and handing on effects (`TypedTextCanvasTests`), drafts above the active layer (`ShapeCanvasTests`, `TypedTextCanvasTests`; inside a clipping stack the draft is drawn by `drawOwnWithDraft` inside the stack's group, as Swift's), new text when the active layer cannot draw, effects and placed masks kept while brushing, painting masks, dragging gradients and moving pixels (`EffectsCanvasTests`, `MaskEffectsCanvasTests`, `BrushCanvasTests`, `MaskPaintAnywhereTests`): held. `distortShape`, the now-internal `imageCorners` and `editedText`/`handOffTextEffects` are GPU plumbing or a refactor of what the twin already draws. Known difference: Swift moves the pixel grid and a new text frame into an overlay view below its transform overlay, so the grid draws over the document's border hairline and the text frame under the other overlays; the twin draws the grid inside the document under the hairline and the text frame last. Both sit above the pixels and take no input; the order shows only on the hairline's edge pixels (a 13% white line), and no other overlay shows while a text frame is dragged. A port was tried and left: QPainter's antialiased hairlines make the order unprovable to a level. `4c5e1a6`, `d04f159`: texture retention, eviction and blits are GPU mechanisms; the CPU's sharp reductions are `DownsampleCache`'s, pinned by `DownsampleTests`. `b1b7490` only words a test's diagnostics. Tests tried and left: a distortion's preview against its commit differs on noise (the commit resamples the warp at its trimmed place), which Swift's tests never claim (they compare two previews); `DistortCanvasTests` pins the preview. Mutants: the region's mapping and use in `LiveMaskRenderer::adjust` and each term of the canvas's `adjustmentRegion`: 6, all caught (the width and height terms only once the partial repaint was compared). Judgments (each Swift contract, held unless named above): +- Blending (13.9a: `6dc2219`, `8ab32c9`, `5a8f6ce`, `e6c964b`). Held, proven: clipping stacks and live-mask coverage at device resolution (the twin's `LiveMaskRenderer` has no `bounds`: its group, alpha and coverage surfaces are the painter's device since 4.x, so Swift's `resolution` and `pixelSize` have no twin); an adjustment in any mode, over the document and inside a stack, blended at full coverage, its alpha restored, then mixed through mask and opacity (`adjust`'s `blended` goes through `LayerRenderer::composite`, which hand-blends every mode QPainter lacks through `HslBlend`, so Swift's `SeparableBlend.blend` has no twin); a clipping stack composited in its base's mode in every mode (`drawComposite` hands `m_stackModes` to `LayerRenderer::composite`, the same path), on the canvas, in export and in Copy Merged alike. Ported: Soft Light by Photoshop's formula, as Core Image computes it (2bs + b²(1 − 2s) up to half, 2b(1 − s) + √b(2s − 1) above), in `SeparableBlend::channel`, and `needsSurface` names Soft Light, so `HslBlend` blends it by hand everywhere a layer, a stack or an adjustment is drawn; `LayerRenderer::compositionMode` loses its Soft Light case (dead now). Compared first: Qt's `CompositionMode_SoftLight` is the PDF's (W3C) formula, which equals Photoshop's below a source of half and above it wherever the backdrop passes 0.25; under that it takes the PDF's cubic for √b and runs up to 17 levels darker (backdrop 5/255, source 1: 19 where Photoshop gives 36). Swift's 0.9 over 0.5 gives 170 under either formula. Known difference: Swift's Core Image is "within 5 levels of Photoshop's"; the twin is Photoshop's own formula, so it may differ from Swift's by those few levels. Tests: `BlendingTests` (Swift's `clippingStacksBlendInTheirBasesMode` for Linear Dodge, Color Dodge and Multiply: Swift's numbers, Copy Merged equal to export, the canvas within a level; adjustments in Difference and Linear Light over the document and Vivid Light and Subtract inside a stack, by the numbers, at full and half opacity, in all three places; Swift's `softLightMatchesPhotoshop` plus a dark backdrop and the lower branch; Swift's `softEdgedLayersBlendTheSame` for Soft Light, Overlay, Multiply and Color Dodge: a radial soft layer over noise, Copy Merged equal to export, the canvas within a level), `BlendModeTests::everyBlendModeMatchesThePdfFormulas` (its Soft Light reference is Photoshop's now; its backdrop of 0.1 under sources past half catches the PDF's cubic (at 0.25 the cubic equals √0.25)), `ClippingStackTests::stacksAndCoverageTakeTheDevicesPixels` (a half-unit stripe stays sharp at a device scale of two, in a stack and through coverage from a source further down). Equivalent mutant: Soft Light's `source <= 0.5` for `< 0.5` (both branches give the backdrop at a half). Judgments: stacks and coverage at device resolution, adjustments in any mode and a stack in its base's mode were held (proven by the tests above); Soft Light ported. Mutants: 5 on Soft Light, 4 caught, one equivalent (above). +- The Metal canvas (13.9b: `cdf8674`, `4b04323`, `5708ed3`, `93de9e7`, `54dd707`, `91cb0b1`, `f773f7b`, `4c5e1a6`, `d04f159`, `b1b7490`). OmaPhoto stays a CPU canvas (the port's decision): Swift's GPU canvas, its `GPUCanvas`, `GPUNoise`, `MetalWarp` and `CanvasLinesOverlay` and the GPU branches of `EditorCanvas`, have no twin; the twin's canvas is the Core Graphics canvas those commits compare against. Ported: Grain and Add Noise anchored to the document (`54dd707`): `LiveMaskRenderer::adjustmentRegion` (Swift's `adjustmentRegion`) maps what the painter shows into the document, and the canvas's `drawLayers` sets it from its centre mapping and zoom, so the smooth path counts noise in frame pixels from the document's corner and Grain in document pixels, as export does, and the crisp path's surface maps to document pixels, so a partial repaint keeps one field; the blur's scale still comes from the unmapped region. This retires 9.5b's and 12.8c's known differences (Grain and noise following the view, a partial repaint's own field); `CanvasDisplayTests::grainFollowsTheViewOnTheCanvas` is gone. Export and `drawLiveComposite` already draw in document pixels and set no region, as Swift's. Tests: `AdjustmentCanvasTests` (Swift's `matchesEveryAdjustment`: every kind through a ramp mask at 80% over a photo and a translucent layer, the canvas within a level of the export over its checkerboard; Swift's `patternsStayWithTheDocument` for Add Noise and Grain: equal to export at 100% and 200%, panned and not, and at 80% a whole-pixel pan moving the pattern with the image and a partial repaint showing the whole's pattern within two levels of resampling), `CanvasEditContractTests` (Swift's `aNewMaskRevealsTheWholeLayer`, upright and turned: a one-pixel mask on a scaled layer changes no covered pixel, the canvas as the export; `matchesWhileMovingPixels`, moving and duplicating on a masked layer at 60% under a soft selection: the preview equals the commit and the export outside the ants, the hole only for a move; `matchesWhileDraggingAGradient` in a selection at 70%: the pending gradient equals its commit outside the overlays, nothing outside the selection; `5708ed3`'s failed fill cancelling with `brushError`, no step, the document unchanged; `matchesALiveMaskOutsideAClippingStack`: a turned source with a layer between, the canvas as the export), `CanvasContractFixtures.h` (the scene, the export over the canvas's checkerboard, a gap that skips the overlays' bands, the ramp mask). Judgments, each Swift contract held unless named above: `cdf8674`: rotation, flips, scale, sharp successive reductions, premultiplied blending in every mode with layer and ancestor opacity, the checkerboard anchored to the document, the crisp composite at high zoom, sampling modes and rounded-up reduction extents, premultiplied blending in sRGB, the surround, shadow and border, placed own masks and multiplied folder masks on pixel layers, filter, Levels and Hue/Saturation previews in the layer's place: held; the CPU paths are the reference and are pinned by `LayerRendererTests` (placement, sampling rules, halvings, overhang), `DownsampleTests`, `BlendModeTests`, `CanvasDisplayTests` (checkerboard, shadow and border, crisp from 200%, the rich scene against the export), `FolderOpacityTests`, `FolderMaskClipTests`, `MaskPlacementTests`, `LevelsCanvasTests`, `HueSaturationCanvasTests`, `VignetteCanvasTests`. `4b04323`: a moved or duplicated selection's preview and commit: held and now proven on a masked, translucent layer (above); the rounded offset is pinned by a move of 5.6 landing at 6 (`EffectsCanvasTests::movingPixelsKeepTheEffects`). Swift's laziness (`PixelMove.applyOffset`, `holed`, `liftedTarget`) only spares the GPU canvas a rebuild per move; the twin rebuilds eagerly in `movePixels`, which realises the tiles before display and commit by construction, and a failure there cancels the move with `brushError` (`PixelMoveTests::aPixelMoveHoldsTheSessionAndAFailedMoveCancels`). `5708ed3`, `91cb0b1`: live paint, erase and mask paint, paint past the old bounds under the old mask (`TiledStrokeTests`, `BrushPreviewTests`, `MaskPaintAnywhereTests`), Smudge and Liquify previews through the mask, opacity and placement (`SmearCanvasTests`, `FolderOpacityTests`): held. `GradientEdit.Fill` and `applyFill` are the GPU's laziness; the twin fills in `refreshGradient`, before display and commit by construction, a failure cancelling with `brushError` (now tested), and the commit publishes what was filled. `MetalWarpTests.matchesTheCPU`: the twin's `WarpStroke` is the CPU reference (`SmudgeLiquifyTests`); nothing to port. `93de9e7`: a one-pixel white mask revealing its layer: held (QPainter's smooth draw clamps an image's edge, as Core Graphics draws it; now tested); the live paint grid's extent and edge sampling: held (`TiledStrokeTests`, `DownsampleTests`, `TiledLayerTests`). `clampedToExtent` is GPU-only. `54dd707`: every adjustment kind with alpha, masks and opacity, blurs at the drawn scale and direction, adjustment results equal to the CPU reference, deterministic seeds: held (the canvas runs the CPU filters themselves; now tested for every kind); Grain and Add Noise anchoring: ported (above). `f773f7b`: non-adjacent live-mask sources (now tested), distortion previews (`DistortCanvasTests`, `DistortMaskTests`, `EffectsCanvasTests`), text previews keeping and handing on effects (`TypedTextCanvasTests`), drafts above the active layer (`ShapeCanvasTests`, `TypedTextCanvasTests`; on a stack's base the draft is drawn by `drawOwnWithDraft` inside the stack's group, as Swift's: it joins the base, alpha included, under the clipped layers: `CanvasEditContractTests::aDraftOnAStacksBaseSitsInsideTheStack`), new text when the active layer cannot draw, effects and placed masks kept while brushing, painting masks, dragging gradients and moving pixels (`EffectsCanvasTests`, its `movingPixelsKeepTheEffects` and `aGradientKeepsTheEffects` for Swift's `movingWithEffects` and `gradientWithEffects`, `MaskEffectsCanvasTests`, `BrushCanvasTests`, `MaskPaintAnywhereTests`): held. `distortShape`, the now-internal `imageCorners` and `editedText`/`handOffTextEffects` are GPU plumbing or a refactor of what the twin already draws. Known difference: Swift moves the pixel grid and a new text frame into an overlay view below its transform overlay, so the grid draws over the document's border hairline and the text frame under the other overlays; the twin draws the grid inside the document under the hairline and the text frame last. Both sit above the pixels and take no input; the order shows only on the hairline's edge pixels (a 13% white line), and no other overlay shows while a text frame is dragged. A port was tried and left: QPainter's antialiased hairlines make the order unprovable to a level. `4c5e1a6`, `d04f159`: texture retention, eviction and blits are GPU mechanisms; the CPU's sharp reductions are `DownsampleCache`'s, pinned by `DownsampleTests`. `b1b7490` only words a test's diagnostics. Tests tried and left: a distortion's preview against its commit differs on noise (the commit resamples the warp at its trimmed place), which Swift's tests never claim (they compare two previews); `DistortCanvasTests` pins the preview. Mutants: the region's mapping and use in `LiveMaskRenderer::adjust` and each term of the canvas's `adjustmentRegion`: 6, all caught (the width and height terms only once the partial repaint was compared). - 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. @@ -183,8 +183,8 @@ Platform adaptations, each forced by a missing Qt analog: - `CompositorMenus` builds the window's `QMenuBar` from Swift's `.commands`, entry for entry, as far as the session functions exist (`TASKS.md`, "Menu items owed", lists the rest); ⌘ is Ctrl and ⌥ is Alt in every shortcut. Its entries follow the front tab's session, like the toolbar. Swift's Undo and Redo keep a text field's own undo while one has focus, so the twin's do the same: with a `QLineEdit` in focus the entries are the field's, titled bare and always enabled; otherwise they carry the step's name, and only while the session's history is usable. Focus is state the session does not announce, so the menus also sync on `QApplication::focusChanged`. Keyboard navigation (Alt+E) hands focus to the menu bar itself, so the menus keep the field that held it until focus lands anywhere else. The View menu's Pixel Grid entry is checkable and follows `showsPixelGrid`. `Close Project` closes the tab (Swift's `close(window)` hands over to the workspace); `Delete Layer` is Swift's `deleteLayerOrMask`, which takes a chosen effect first since 9.6a; since 9.6d its title names that effect (`Delete Stroke`). The `Hide` entries and `Check for Updates` have no twin (the desktop hides windows; distro packages update). - The session's gates (`canEditLayers`, `canUseHistory`) and its `selectLayer`, `selectTool`, `undo` and `restore` read only the state that exists so far. `TASKS.md` lists, under "Session hooks owed", the term each later item adds. `reorderLayers` takes a `std::set` for `IndexSet` and reproduces SwiftUI's `move(fromOffsets:toOffset:)`. - `LayerMask::clipImage` returns nil when the placed grid cannot be rendered, as Swift's `try?` does, and logs a warning on `omaphoto.rendering` so the loss is visible. -- `Rendering/SeparableBlend` twins Swift's file for the modes QPainter lacks since 1.2.2: `needsSurface` names Linear Burn, Linear Dodge (Add), Vivid Light, Linear Light, Pin Light, Hard Mix, Subtract and Divide, and `channel` is Photoshop's formula for each on one channel (in Vivid Light a zero divisor is infinity, clamped to the end; Divide by black is white, zero over zero included, a choice of the twin's, since neither Photoshop nor Core Image publishes it). Swift reaches Core Image's filters, in sRGB as Color Burn and Dodge have been since 1.2.0 (`121a15f`); Color Burn and Dodge themselves stay QPainter's, which follow the PDF formulas, as do its Hard Light and Exclusion (`BlendModeTests::everyBlendModeMatchesThePdfFormulas` holds all twenty-four modes within 2/255 against formulas written apart). Known difference: Core Image's code is not published, so its edges (Hard Mix exactly at one, a divisor of zero) follow Photoshop's definitions, not measured Core Image. Hard Mix is Photoshop's at full fill, white where backdrop and source reach one together. -- `Rendering/HslBlend` is Linux-only: the modes QPainter lacks (Hue, Saturation, Color and Luminosity, and since 1.2.2 `SeparableBlend`'s eight), blended by hand over a copy of the backdrop with the PDF's compositing, so every path that hands a mode to `HslBlend::handles` (the export, the canvas's surface, live masks, effects) takes the new ones. +- `Rendering/SeparableBlend` twins Swift's file for the modes QPainter lacks since 1.2.2: `needsSurface` names Soft Light (since 13.9a), Linear Burn, Linear Dodge (Add), Vivid Light, Linear Light, Pin Light, Hard Mix, Subtract and Divide, and `channel` is Photoshop's formula for each on one channel (in Vivid Light a zero divisor is infinity, clamped to the end; Divide by black is white, zero over zero included, a choice of the twin's, since neither Photoshop nor Core Image publishes it). Swift reaches Core Image's filters, in sRGB as Color Burn and Dodge have been since 1.2.0 (`121a15f`); Color Burn and Dodge themselves stay QPainter's, which follow the PDF formulas, as do its Hard Light and Exclusion (`BlendModeTests::everyBlendModeMatchesThePdfFormulas` holds all twenty-four modes within 2/255 against formulas written apart). Known difference: Core Image's code is not published, so its edges (Hard Mix exactly at one, a divisor of zero) follow Photoshop's definitions, not measured Core Image. Hard Mix is Photoshop's at full fill, white where backdrop and source reach one together. +- `Rendering/HslBlend` is Linux-only: the modes QPainter lacks (Hue, Saturation, Color and Luminosity, and `SeparableBlend`'s nine: eight since 1.2.2, Soft Light since 13.9a), blended by hand over a copy of the backdrop with the PDF's compositing, so every path that hands a mode to `HslBlend::handles` (the export, the canvas's surface, live masks, effects) takes the new ones. - Known difference: CoreGraphics enlarges "High quality" layers with a sharper kernel; QPainter offers bilinear only, so `InterpolationQuality::high` and `low` draw alike when enlarging. Reductions are sharp on both (halvings). - Where an Apple framework threads internally (vImage, Core Image, Metal), the twin threads with QtConcurrent. - A Swift error enum becomes an exception class with a `Kind` enum; `what()` carries the Swift `errorDescription`. diff --git a/src/Rendering/HslBlend.h b/src/Rendering/HslBlend.h index add8d91..8ce22e9 100644 --- a/src/Rendering/HslBlend.h +++ b/src/Rendering/HslBlend.h @@ -2,7 +2,7 @@ #include "Document/LayerAppearance.h" #include -// Blend modes QPainter lacks: the four non-separable, SeparableBlend's eight. +// Modes QPainter lacks: four non-separable, SeparableBlend's nine. namespace HslBlend { bool handles(LayerBlendMode mode); // Source over backdrop, same size, both RGBA8888 premultiplied. diff --git a/tests/AdjustmentLayerTests.cpp b/tests/AdjustmentLayerTests.cpp index c30e0b7..975bd00 100644 --- a/tests/AdjustmentLayerTests.cpp +++ b/tests/AdjustmentLayerTests.cpp @@ -187,7 +187,7 @@ void AdjustmentLayerTests::adjustmentBlendAndSoftMaskPreserveCoverage() s.updateAdjustment(id, value); s.setLayerBlendMode(LayerBlendMode::multiply); QVERIFY((rendered(s) == std::vector{0, 0, 0, 255, 0, 0, 0, 128, 0, 0, 0, 32, 0, 0, 0, 0})); - // Green over red adds to yellow; Swift adjusts as Normal. + // Green over red adds to yellow, as Swift 1.4. s.setLayerBlendMode(LayerBlendMode::linearDodge); QVERIFY((part(rendered(s), 0, 4) == std::vector{255, 255, 0, 255})); QCOMPARE(rendered(s)[7], 128); diff --git a/tests/CanvasEditContractTests.cpp b/tests/CanvasEditContractTests.cpp index 23540b0..391f640 100644 --- a/tests/CanvasEditContractTests.cpp +++ b/tests/CanvasEditContractTests.cpp @@ -34,6 +34,7 @@ private slots: void aGradientInASelectionShowsAsItsCommit(); void aGradientThatCannotFillCancels(); void aLiveMaskFromFurtherDownShowsAsTheExport(); + void aDraftOnAStacksBaseSitsInsideTheStack(); }; void CanvasEditContractTests::aNewMaskRevealsTheWholeLayer_data() @@ -173,5 +174,29 @@ void CanvasEditContractTests::aLiveMaskFromFurtherDownShowsAsTheExport() QVERIFY2(scene.gapToExport() <= 1, qPrintable(QString::number(scene.gapToExport()))); } +// The draft follows its active base into the stack's group. +void CanvasEditContractTests::aDraftOnAStacksBaseSitsInsideTheStack() +{ + ContractScene scene(60, 40, 60, 40); + const QImage white = solid(30, 40, qRgba(255, 255, 255, 255)); + scene.session.insert(ImportedImage(white, white, QStringLiteral("Base")), QPointF(15, 20)); + const QUuid base = scene.session.activeLayerID().value(); + const QImage red = solid(10, 40, qRgba(255, 0, 0, 255)); + scene.session.insert(ImportedImage(red, red, QStringLiteral("Clipped")), QPointF(5, 20)); + QVERIFY(scene.session.linkMask(base, scene.session.activeLayerID().value())); + scene.session.zoom(1); + scene.session.selectLayer(base); + scene.session.selectTool(NavigationTool::shape); + scene.session.beginShape(QPointF(5, 10)); + scene.session.dragShape(QPointF(55, 30), false, false); + const QImage shown = scene.documentShot(); + // The draft joins the base, its alpha included, as Swift's. + QCOMPARE(shown.pixel(20, 20), qRgb(0, 0, 0)); + QCOMPARE(shown.pixel(45, 20), qRgb(0, 0, 0)); + // The clipped layer stays over it. + QCOMPARE(shown.pixel(5, 20), qRgb(255, 0, 0)); + scene.session.cancelShape(); +} + QTEST_MAIN(CanvasEditContractTests) #include "CanvasEditContractTests.moc" diff --git a/tests/EffectsCanvasTests.cpp b/tests/EffectsCanvasTests.cpp index 61eef8f..409e749 100644 --- a/tests/EffectsCanvasTests.cpp +++ b/tests/EffectsCanvasTests.cpp @@ -3,7 +3,9 @@ #include "Rendering/EditorCanvas.h" #include "RenderFixtures.h" #include "EffectsCanvasFixtures.h" +#include "SelectionFixtures.h" #include "SessionFixtures.h" +#include #include // Swift's effects on the canvas: previews, the paint's surface, distortions. @@ -61,6 +63,8 @@ private slots: void paintPastTheSurfacesBudgetShowsTheLastEffects(); void invalidEffectsShowNothing(); void effectsDrawAheadOfAColourEditsPreview(); + void movingPixelsKeepTheEffects(); + void aGradientKeepsTheEffects(); }; void EffectsCanvasTests::theCanvasDrawsALayersEffectsOnceTheyLand() @@ -354,5 +358,84 @@ void EffectsCanvasTests::effectsDrawAheadOfAColourEditsPreview() QCOMPARE(scene.at(QPoint(23, 20)), yellow); } +// A band round a path, where overlays draw. +QPainterPath band(const QPainterPath &path, double width) +{ + QPainterPathStroker stroker; + stroker.setWidth(width); + return stroker.createStroke(path); +} + +// The document as shown, pixel by pixel, overlays' bands aside. +int gapBetween(Scene &scene, const QImage &one, const QImage &other, const QPainterPath &skip) +{ + int worst = 0; + for (int y = 0; y < 40; ++y) { + for (int x = 0; x < 60; ++x) { + if (skip.contains(QPointF(x + 0.5, y + 0.5))) + continue; + const QRgb a = one.pixel(scene.shown(QPointF(x + 0.5, y + 0.5))), b = other.pixel(scene.shown(QPointF(x + 0.5, y + 0.5))); + worst = std::max({worst, std::abs(qRed(a) - qRed(b)), std::abs(qGreen(a) - qGreen(b)), std::abs(qBlue(a) - qBlue(b))}); + } + } + return worst; +} + +// Swift's movingWithEffects: the stroke follows the lifted pixels. +void EffectsCanvasTests::movingPixelsKeepTheEffects() +{ + Scene scene; + EditorSession &session = scene.session; + session.setEffects(outline(2)); + scene.landed(); + session.applySelection(rectPath(QRectF(25, 15, 10, 10)), SelectionMode::replace, QStringLiteral("Select")); + QVERIFY(session.beginPixelMove()); + // 5.6 rounds to 6: the square moves to 31..41. + session.movePixels(QSizeF(5.6, 0)); + scene.canvas.synchronizeDisplay(); + QCOMPARE(scene.at(QPoint(42, 20)), yellow); + QCOMPARE(scene.at(QPoint(24, 20)), QColor(Qt::white)); + const QImage moving = scene.shot(); + bool done = false; + session.finishPixelMove([&done] { done = true; }); + QTRY_VERIFY(done); + QCOMPARE(session.selection().value().path.boundingRect(), QRectF(31, 15, 10, 10)); + scene.canvas.synchronizeDisplay(); + QCOMPARE(gapBetween(scene, moving, scene.shot(), band(rectPath(QRectF(31, 15, 10, 10)), 4)), 0); + session.deselect(); + QTRY_COMPARE(scene.gapToExport(), 0); +} + +// Swift's gradientWithEffects: the pending fill shows its effects. +void EffectsCanvasTests::aGradientKeepsTheEffects() +{ + Scene scene; + EditorSession &session = scene.session; + session.setEffects(outline(2)); + scene.landed(); + // Black fading to clear across the square's left half. + session.applySelection(rectPath(QRectF(10, 5, 20, 30)), SelectionMode::replace, QStringLiteral("Select")); + session.selectTool(NavigationTool::gradient); + session.beginGradient(QPointF(15, 20)); + session.moveGradient(std::nullopt, QPointF(30, 20)); + session.endGradientDrag(); + scene.canvas.synchronizeDisplay(); + // The stroke rings the new paint at once. + QVERIFY(scene.at(QPoint(8, 20)).red() > 100 && scene.at(QPoint(8, 20)).blue() < 30); + const QImage pending = scene.shot(); + bool done = false; + session.commitGradient([&done] { done = true; }); + QTRY_VERIFY(done); + scene.canvas.synchronizeDisplay(); + // The line, its ends and the ants aside. + QPainterPath line; + line.moveTo(15, 20); + line.lineTo(30, 20); + const QPainterPath overlays = band(rectPath(QRectF(10, 5, 20, 30)), 4).united(band(line, 24)); + QTRY_VERIFY(gapBetween(scene, pending, scene.shot(), overlays) <= 1); + session.deselect(); + QTRY_COMPARE(scene.gapToExport(), 0); +} + QTEST_MAIN(EffectsCanvasTests) #include "EffectsCanvasTests.moc"