Files
OmaPhoto/AGENTS.md
T

381 KiB
Raw Blame History

OmaPhoto — agent notes

OmaPhoto is the Linux port of Compositor, the macOS app at https://github.com/robbietilton/Compositor. That repository's Swift tree (Compositor/, CompositorTests/) is the specification; this repository mirrors it. Its checkout at ../Compositor-spec, pinned to the tag being ported (v1.3.3 since item 12), is where the Swift sources are read; never edit it for OmaPhoto work.

Designing or editing a project

To make or change an image in a .comp project, the source is not needed: read docs/writing-comp-files.md. It covers the format, the rules that make a project load, and writing it safely while it is open. docs/project-format.md describes the whole format.

Trees

  • The repository root: the app. C++20, Qt 6 Widgets (floor Qt 6.4), CMake, Qt Test. CPU raster only: QPainter plus the C kernels in kernels/, copied unchanged from Compositor's Compositor/Rendering/*.c and compiled in place.
  • packaging/: the desktop entry, MIME type, icons (Compositor's app icon) and the Arch PKGBUILD; flake.nix at the root for NixOS.
  • scripts/install/: one install script per distribution family, each fetching the latest release's package.
  • .github/workflows/release.yml: a v* tag builds the DEB, RPM and Arch package and publishes them.
  • TASKS.md: ordered task list and progress truth. Its entries before 11.4b speak of the port's old home, linux/ inside Compositor's repository.

Names: the app is OmaPhoto (binary omaphoto, app id io.github.ZacharyZhang_NY.OmaPhoto, logging omaphoto.*, build options OMAPHOTO_*). Compositor's project format stays whole (.comp, application/x-compositor-project), so projects move between the two apps; C++ types keep their Swift twins' names (CompositorApp, CompositorMenus).

Mirror rule

Each Swift file has one Linux twin under src/ in the same folder (Document, Rendering, UI, IO) with the same type and function names. Tests in tests/ twin CompositorTests/. A Swift file over 500 lines splits as Name+Part. A Swift extension of EditorSession declares its members in Name+Session.h (EditorSession+Brush.h and EditorSession+Projects.h for the extensions named after the session): C++ declares members inside the class, so EditorSession.h, the twin of EditorSession.swift with every stored property, includes each fragment inside its body. A fragment holds declarations alone, its extension's private helpers under private:; state stays in EditorSession.h, as Swift keeps stored properties in the class. Qt types are used directly: QImage for CGImage, QRectF, QPointF, QTransform, QUuid. No wrapper types. Nothing outside UI/ includes UI/ headers, except where the Swift source does.

Platform adaptations, each forced by a missing Qt analog:

  • ImportedImage exposes pixels through image(), size through size(), identity through identity(). CoreGraphics backs a painted layer's CGImage with a lazy data provider; Qt has none. image() flattens painted tiles on first use, so commits, display and history never flatten a layer. Never cache its result in a model type.
  • Image identity (=== on CGImage) is ImageIdentity: the RasterSnapshot pointer for painted assets, QImage::cacheKey() otherwise.
  • Layer pixels are QImage::Format_RGBA8888_Premultiplied; masks are QImage::Format_Grayscale8. BrushRaster::context makes both.
  • Contexts are top-left already, so the CoreGraphics y-flips vanish. BrushRaster::draw has no mask flag: both CoreGraphics paths are a plain copy in Qt. It takes an optional source part instead of CGImage.cropping(to:), which Qt cannot do without a copy.
  • BrushRaster::visibleRect stands in for boundingBoxOfClipPath.
  • Images are drawn into explicit target rects. The positional drawImage(x, y, image) scales by the source's device pixel ratio; CoreGraphics never does.
  • DownsampleCache halves with its own Lanczos-3 filter (vImage has no Linux twin), threaded with QtConcurrent. Swift's label-overloaded image(_:drawnAt:) and image(_:level:) are imageDrawnAt and imageAtLevel. Its constructor takes a pixel budget so eviction can be tested; the app uses shared().
  • LayerRenderer::draw takes Swift's labelled arguments as an Options struct. QPainter has no mask clip, so a masked layer is composed aside in device pixels; the mask is drawn with the same placement into a zero-filled Alpha8 surface and multiplied in 1:1 (DestinationIn), so nothing outside the mask survives; the result is composited with the layer's opacity and blend mode. Work that can throw finishes before save(). LayerRenderer needs a painter on a QImage.
  • CoreGraphics multiplies nested mask clips inside the context; QPainter has no soft clip. FolderMaskClip::apply multiplies a folder mask into device-sized Alpha8 coverage, FolderMaskClip::draw hands each layer the product of its folders' coverage (memoized per folder), and draw routines take it as LayerRenderer::Options::clip.
  • TiledLayerRenderer keeps Swift's pieces, interiors, margins and piece cache, but not its clip surgery. CoreGraphics needs a transparency layer, cleared interiors, subtracted clip paths and device snapping so tiles blend exactly once; the twin draws the base, then for each piece clears and redraws the device pixels whose centres lie in its interior, a half-open QRegion computed per scanline, so neighbouring pieces share no pixel and leave none out at any rotation, and the region stays hard while the image keeps its antialiased outline (a path clip turns soft once Antialiasing is on). Where an interior ends with its image, the region opens one device pixel outward for that outline, measured through the painter's whole device transform, so it holds at any reduction and under unequal painter scales; the interior already spans the halving overhang there. All of it happens inside LayerRenderer::composite, which applies opacity, blend mode, mask and folder clip once. QPainter steps texture coordinates in fixed point, so a piece can differ from the whole image by 3/255 on noise (the tests allow 4 upright, 12 rotated as Swift does). A layer edge off the halving grid differs more, on macOS too: a piece keeps the halving spill past that edge, a whole image crops it (10/255 measured at 0.05×).
  • Live strokes reuse that design. LayerRenderer::composite takes an optional veil callback that repaints the mask coverage after the mask is drawn (coverage starts full when the layer has no mask). drawStroke draws the unchanged pixels, then its pieces; where a piece reaches past the old bounds it repaints the coverage with a mask piece (white, then the old mask), so paint there is revealed, where Swift draws a second unmasked pass with holes. drawMaskStroke draws the layer once and lets mask pieces replace their interiors of the coverage, where Swift redraws the layer clipped to each piece. A commit's larger pieces take over stretches of the old outline that the base drew during the stroke; the base crops its halving spill, so that outline changes by up to 12/255 on commit, on macOS too.
  • LayerRenderer::drawBrushPreview replaces Swift's even-odd clips with CompositionMode_Source copies into the aside surface, hard-edged (composite runs with nearest sampling and the layer's interpolation quality); the source stays clipped to the grid and a raster's base to the source bounds, as Swift's clips keep them. Its BrushPreview struct carries Swift's labelled arguments. A raster counts as a source by itself, so a caller never flattens a painted asset just to pass a non-nil image. It throws std::logic_error on an empty pixel grid, where Swift would divide by zero.
  • LiveMaskRenderer has no bounds. Swift allocates its coverage and group surfaces over the clip's bounding box, one pixel per user unit, and clips with CoreGraphics; the twin's surfaces are the painter's device in device pixels, drawn with the painter's device transform, because clip coverage must be what Options::clip takes. drawOwn receives that coverage, and an outer clip (folder masks) multiplies in; a clipping stack composites its group one to one through LayerRenderer::composite, under the painter's own opacity as in Swift; draw saves and restores the painter around drawOwn, exceptions included. The canvas keeps this cheap by painting each dirty rect through an image surface (item 4.4). The constructor takes a pixel budget so tests can reach the limit; passing it, or failing to allocate, logs a warning on omaphoto.rendering where Swift skips in silence.
  • AdjustmentSurface::draw allocates what the painter can show in device pixels (Swift: one pixel per user unit), lets the body draw in the painter's own coordinates, and draws the surface back under the painter's own opacity, composition mode and clip, as CGContext.draw inherits them. A surface that cannot be allocated, or passes the budget, logs a warning and draws nothing. On Linux it also gives the hand-blended HSL modes a backdrop on devices without readable pixels (widgets, pixmaps).
  • ImageExporter is a namespace of functions. Swift's actor only serialises calls; the twin keeps no state, and callers run it off the UI thread. Files are QString paths. PNG and JPEG go through QImageWriter with an sRGB tag and the resolution as dots per metre; the JPEG preview is read back with QImageReader, scaled to 1000 pixels at most. write replaces the file through QSaveFile and throws std::runtime_error with the system's message, where Swift rethrows the Cocoa error. Exports log on omaphoto.io.
  • ProjectStore is a namespace too. Swift's synthesized Codable is written by hand in IO/ProjectStore+Manifest.cpp: the same keys, the same refusals (wrong types, missing required keys, non-canonical UUIDs), unknown keys ignored, CGPoint and CGSize as two-number arrays, UUIDs upper case (uuidString), two-space indentation so the 4 MiB limit bites where Swift's does. Asset files carry the upper-case names macOS writes; Linux file systems tell the cases apart.
  • Saving stages a hidden sibling folder, syncs its files and folders to disk, then swaps it in with renameat2(RENAME_EXCHANGE); a first save is a plain rename. Where the system knows no exchange (FAT sticks, old kernels) the store logs a warning and replaces in two moves. Either way the parent folder is synced after the swap, the old project is kept until that sync succeeds, and a failed sync puts it back before the error is thrown. If the way back fails too, nothing is deleted: the old project stays where the swap left it (the hidden sibling, or its .old neighbour after two moves) and a critical log on omaphoto.io names that path. ProjectReplaceTests defines rename, renameat2, fsync and read in the test executable, where they take libc's place, to order and fail each call. There is no NSFileCoordinator. A missing manifest is ProjectError(invalid) and a missing asset missingImage, where Swift passes on a Cocoa error; a failed write throws std::runtime_error with the system's message. QFile reports no error when a read fails after partial data, so the manifest's byte count is checked against the file's size. The manifest's version, width and height are qint64, Swift's Int, read from Qt's exact integer (QJsonValue::toVariant as LongLong), never through a double: a wild number decodes and is refused by its own rule, with every digit of its value in the version error. One fact of Qt's parser stays, checked against Swift 6.0.3's decoder: a number token outside Int64, or one with a fraction or an exponent, passes through a double inside Qt. So the 1,024 integers below Int64's minimum and -9.223372036854775808e18 read as the minimum where Swift refuses them, and 9007199254740993.0 loses its last digit where Swift keeps it. Every such value lies outside each valid range; only the kind or the number of the refusal differs. ProjectTests pins one. A whole number with a fraction or an exponent (5.0, 5e0) reads exactly, as Swift's slow path reads it; a token that underflows a double (1e-400) fails Qt's whole parse as an illegal number, where Swift reads zero. Only a Grain seed (9.5a), every UInt32 of which is valid, turns that into a project Swift opens and the twin refuses; aSeedReadsWhatQtsParserReads pins it.
  • Qt refuses image decodes over 128 MB (QImageReader::allocationLimit). ImageImporter::liftAllocationLimit lifts that limit for the store and the importer: their own pixel budgets are checked from the file headers before any pixel is read.
  • ImageImporter is a namespace; the Swift actor only serialises calls and caches a CIContext. The type comes from the file's bytes, as CGImageSourceGetType reads it: QImageReader::setDecideFormatFromContent for JPEG, PNG and TIFF (TIFF needs qt6-image-formats-plugins), libheif's heif_get_file_mime_type for HEIC (image/heic: HEVC in HEIF). Other ISO media (AVIF, plain HEIF, video) and every other format Qt recognises are unsupported; bytes nothing recognises are unreadable. HEIC decodes through libheif, which applies the file's own turn and crop boxes, so its EXIF orientation is never applied again. Its colours come from the ICC profile, else from the nclx box, which ImageIO reads for Swift: the box's primaries (libheif hands over their chromaticities) with the curve it names: sRGB, linear, gamma 2.2 or 2.8 through Qt's own transfer functions, and BT.709's curve (shared by BT.601, BT.2020, xvYCC and BT.1361 between zero and one) as a table; unspecified primaries or curve count as sRGB's, like untagged pixels. Qt 6.4's QColorSpace constructor that takes points and a table builds an invalid space (probed), so the table is set afterwards. Curves with no conversion here (PQ and HLG, which need tone mapping; SMPTE 240M, SMPTE 428, the logarithmic ones), primaries Qt cannot hold and a box libheif rejects are unreadable, each with its reason in the log: no HEIC imports with colours the importer knows to be wrong. heif_init loads the decoder plugins (Ubuntu ships HEVC decoding as libheif-plugin-libde265); a missing decoder is unreadable, with libheif's message in the log. For Qt's formats the importer reads the pixels as stored, converts them to sRGB while the ICC profile is still attached, and only then applies the EXIF or TIFF orientation itself: Qt 6.4's setAutoTransform drops the colour space. Untagged pixels count as sRGB, as CoreImage counts them. Qt 6.4 reads no orientation from a PNG's eXIf chunk (probed: supportsOption(ImageTransformation) is false), so such a PNG imports as stored; JPEG, TIFF and HEIC turn upright. The result is RGBA8888_Premultiplied, tagged sRGB. A thumbnail's long side is 96 and its short side rounds up, as Swift's extent.integral does. The HEIC fixtures in tests/fixtures are committed; scripts/heic-fixtures.sh makes them and stops at the first failure, because the dev image carries no HEVC encoder. The tests patch the lossless p3-nclx.heic in memory to name other primaries and curves.
  • LayerRenderer::drawCoverage: Swift clips to the mask and fills white; QPainter has no soft clip. The twin builds white whose alpha is the mask (premultiplied, from the halved mask draw would use, since CoreGraphics filters a shrinking mask and QPainter's bilinear filter aliases) and draws it under the painter's own opacity and clip, in the composition mode under which alpha stands for a clip's coverage: the painter's own for every mode that is linear in the source and keeps pixels where the source is clear (source-over, the blend modes, the rest of Porter and Duff's), and its exact stand-in for the seven that are not (Source and Plus become SourceOver, Clear becomes DestinationOut, SourceIn SourceAtop, SourceOut Xor, DestinationIn Destination, DestinationAtop DestinationOver). DrawCoverageTests holds all 24 modes against the oracle, a fill of opaque white mixed in by coverage, over opaque, half clear and clear pixels. Those seven take no painter opacity (CoreGraphics scales the source by it, Qt mixes the result by it, and only linear modes make the two agree): below full opacity they are a logic_error, as is any raster operation. No caller sets a mode today. Source-over that is white through coverage, 255 a + d(1 - a), on gray and colour devices alike. It takes the placement draw takes, radians() included, so a wild angle turns both alike. A mask that is not gray is a logic_error; a white image that cannot be allocated is ExportError(render).
  • QPainter places an unscaled, unturned image on whole device pixels whatever the hints say; CoreGraphics would blur it by the fraction. Layers the app makes sit on whole pixels, Shape lines aside (8.8), and any scale or turn takes QPainter's filtered path, so tests of soft edges draw under a scale.
  • ImageResizer and CanvasResizer are namespaces. Task.checkCancellation() in the Swift resizer has no twin: no caller cancels a resize. Each turns a std::bad_alloc into ExportError(render) at its door (since 10.1 and 10.2), so the workers that run them catch it. A layer's new box is judged by LayerTransform::isValid before any number becomes an integer, where Swift converts first: Int(Double) traps on a wild value, C++ would be undefined. Records are rebuilt field by field as Swift rebuilds them, so a field Swift leaves out is dropped once that field exists (the image resizer passes on neither shape, effects nor text; the canvas resizer passes on shape and text, not effects). Options hold qint64, Swift's Int, like the manifest. CanvasSizeDraft keeps plain members where Swift has let, so the struct stays assignable.
  • EditorSession is a QObject. One signal, changed(), stands in for Swift's observation: every method that changes state emits it, a refused request stays silent, and widgets compare what they show with what the session holds, as CanvasView.synchronizeDisplay does. beginEdit emits as endEdit does, because an open transaction closes canUndo and canRedo. fit and zoom emit only when the view changed, so a zoom that is no number stays silent. The canvas edits viewport in place and then calls notify(). A Swift property with a didSet is a private member with a getter and a setter (activeLayerID, isProjectBusy, showsNewDocument, showsImporter, isImporting, importError, renamingLayerID); document, tool, collapsedGroupIDs and selectedLayerIDs are read through getters and change only through session methods; history and viewport are public members, as Swift's tests use them. activeLayer() returns a copy, as Swift's computed property does. EditorSessionSignalTests holds the signal's contract: every change is announced, a refusal is silent, and the last signal of a change sees the state the call leaves behind, read inside the slot.
  • EditorSession.swift runs to 741 lines, so the twin splits: EditorSession+Model.h/.cpp hold ImageLayer, CanvasDocument and NavigationTool (their own header also breaks the include cycle with DocumentHistory, which a member by value needs complete), EditorSession.h/.cpp the core, EditorSession+Layers.cpp the layer operations, EditorSession+Transform.cpp the transform edits, EditorSession+Imports.cpp the import queue and the busy gates; EditorSession+Projects.cpp twins the Swift file of that name. A session function that Swift defines in an extension lives in the twin of that Swift file, declared in its Name+Session.h: the LayerGroups extension (folders, the multi-selection, grouping, placing, layerRows) in LayerGroups.cpp, finishDeletingLayer(s) in LiveLayerMask.cpp.
  • groupSelectedLayers follows Swift's steps (what stays keeps its place, the folder sits at the topmost selected branch, the wrapped layers go last in hierarchy order) with two terms dropped because they are dead in the twin: the nil that ends each ancestor chain (the twin's shared parent starts as the root) and the clamp of the insertion index (it counts layers that stay, so it cannot pass their number). placeLayer drops Swift's target != id for the same reason: the target is sought after the layer is taken out, so a layer is never found above itself. canPlaceLayer is tested by itself as well as through placeLayer, whose validation would hide its terms; the panel asks it before a drop. selectLayers and extendSelection fall back on any selected layer, as Swift's Set.first does: tests never depend on which. A session's document always passes LayerHierarchy::validate (every mutator checks before it commits), so every parentID names a folder of the document; groupSelectedLayers reads parents through std::map::at, which throws if that ever breaks.
  • Transform edits: TransformEdit holds what exists so far (layerID, draft, group); persistent, corners, mask and floating arrive with the items that read them (TASKS.md, "Session hooks owed"). A draft changes only through previewTransform, which refuses an invalid value, and begins as a valid transform or box, so it is valid by construction. commitTransform therefore gives each layer what pendingTransform gives it, one definition for preview and commit, inside one transaction, and skips a layer whose own result is invalid (a box squeezed until a member falls under a pixel); Swift's edit.draft.isValid and firstIndex guards are left out, since nothing can reach them. An edit that moves nothing records nothing, as in Swift: the history drops a transaction that changed nothing. canTransform and groupTransformMembers drop Swift's isGroup terms (a folder never holds pixels, LayerHierarchy::validate sees to it, so asset says the same), and beginTransform drops transformEdit == nil (canTransform reads it through canEditLayers). groupTransformBox keeps max(1, …): rounding leaves a one-pixel layer at origin 1.3 a box under one pixel, and a test holds that. DistortWarp::corners came forward from 6.2 for the group box.
  • Swift's async session calls (importImages, waitForProjectAccess, waitForFileRequest) take a completion callback in the twin. A waiter runs at once when the session allows it; otherwise it is kept, and the setter that frees it posts each waiter to the event loop (Qt::QueuedConnection), where it looks again when its turn comes, as a Swift continuation re-enters its while loop: no waiter runs inside the setter, and the first may take the project before the second looks. importImages takes QList<QUrl> (a URL that is no local file is unsupported, Swift's isFileURL guard), decodes one file at a time through QtConcurrent::run and a QFutureWatcher (the worker catches ImageImportError and hands back the asset or the failure text; anything else is a bug and is left to crash), inserts on the UI thread, and wraps each request in one Import Images transaction. A request's callback is posted to the event loop too, so the caller goes on as a resumed Swift task does: after the transaction ended and, for the last request, after isImporting fell and the failures were published; for an earlier request while the next one decodes. Only a project that stops being busy frees project waiters: saying busy again leaves them in line. showsBusy is a single-shot QTimer of busyIndicatorDelay (250 ms), a Qt::PreciseTimer because a coarse one fires up to a twentieth early, and a child of the session so a test can read its remaining time; tests of time assert floors and states, never ceilings; createDocument clears its two flags and then resumes waiting file requests once, where Swift's two didSets each try. The session therefore needs an event loop: every test that makes one uses QTEST_GUILESS_MAIN.
  • Appearance: allLayerBlendModes stands for Swift's LayerBlendMode.allCases, the order of the blend menu. Swift's refreshCanvasPreview callback has no twin: changed() reaches the canvas, and previewBlendMode emits it only when the preview changes, so hovering over one entry redraws once. blendPreview is a small struct where Swift has a tuple. opacityEditLayerID is a private member: only the session reads it; an open opacity drag shows outside as an open transaction (canUndo is false). Inside a drag setLayerOpacity opens a nested transaction where Swift writes into the open one: the history counts depth, so the drag is still one step, and the branch Swift needs is gone.
  • Projects in the session: Swift's projectURL is projectPath, a QString, since files are paths here; it gets a setter when ProjectController needs one. Swift builds layers and the document through memberwise initialisers in two places (installProject, applyDocumentSize); the twin has two constructors for that, ImageLayer(record, asset, mask) and CanvasDocument(snapshot), so both places agree by construction; shapes and text set them apart: installProject loads both after CanvasDocument(snapshot), applyDocumentSize does not, as Swift's rebuild leaves them out. applyImageSize and applyDocumentSize live in IO/ImageResizer.cpp, as in Swift; they build the new document and record the step on a copy of the history before either is published (DocumentHistory::adopt takes the copy's steps: its past, its future and its revision, all a closed step changes), then fit the view and signal once, so memory that runs out on the way changes nothing (Swift begins, sets, fits and ends, signalling twice; since 10.1). The twin's document is read-only from outside: a test that needs a state no session call can make (a painted mask, a record with every field set) installs a hand-built ProjectSnapshot, where Swift's tests write into session.document.
  • Masks in the session (LayerMask.cpp, after the model): isMaskSelected has a getter only, since every Swift write of it sits in a session extension. TransformEdit gains mask. commitTransform hands a mask-alone edit to commitMaskTransform, which writes the placement inside one transaction and lets the history drop a change that changes nothing, where Swift compares placements first. In copyMask and toggleMaskLink Swift's finishOpacityEdit() after commitTransform() is left out: commitTransform has already finished the drag. tests/SessionFixtures.h holds rewrite(session, change), which installs a changed snapshot where Swift's tests write a mask, a flip or a sampling into session.document; it resets the history as any install does, so steps are counted from there. tests/SessionRecord.h holds described(), the text of everything an observer can read, shared by the signal test files.
  • Live masks in the session (LiveLayerMask.cpp): adoptClipping and releaseDetachedClipping are static members, as Swift's static funcs. removeLiveMask and releaseDetachedClipping walk the layers once per parent in list order, where Swift builds sibling arrays; the runs they free are the same. LiveMaskGraph::validate already refuses a link to oneself, a folder at either end and a missing source, so canLinkMask asks only for editable layers, an existing target and a valid graph; Swift's own checks of those cases, both folder checks of toggleClippingMask and canToggleClippingMask (the layer's own and its lower sibling's: the store validates the graph on load and canLinkMask on every link, so no folder is ever clipped and none becomes a base), and the folder case of the run's base in releaseDetachedClipping (nothing is ever clipped to a folder) are dead and left out; both toggle functions take the base from one helper, clippingBase. adoptClipping keeps its folder check: it writes the link without asking the graph, and LiveMaskEditTests calls it directly, because placeLayer's release hides the side of the rule that looks below. deleteWithLiveMaskChoice shows a QMessageBox from the session, because Swift's session shows an NSAlert there: three buttons with Swift's texts (accept, reject and destructive roles), and LiveMaskDeleteTests drives the real alert from a zero timer under QTEST_MAIN. The bake runs through QtConcurrent::run and a QFutureWatcher with the project busy; layers are deleted first and the project freed last, as Swift's defer does; a failure sets brushError (a getter and a setter, arriving here because this is its first writer) and logs a warning. LiveMaskBaker::bake draws the target in its own pixel grid and each source through the inverse of the target's pixel-to-document map; the target's own mask, opacity and blend mode stay on the layer. A snapshot that repeats a layer id is a logic_error once the target and its pixels are found, where Swift's Dictionary(uniqueKeysWithValues:) traps. The bake's worker catches ExportError alone, as the import's worker catches ImageImportError: anything else is a bug and is left to crash. releaseDetachedClipping keys its runs by the optional parent itself, because all zeros is a valid folder id. PixelAdjust::thumbnail came forward from 9.2 for the baker. Over the renderer's pixel budget a bake draws nothing, on macOS too.
  • ProjectController is a QObject. Its async calls take a completion callback, posted to the event loop like the session's; steps inside continue inline. newCanvas and close take none: no caller awaits them. The store runs through QtConcurrent::run with one QFutureWatcher a call, a child of the controller, so an answer dies with it; the worker catches std::runtime_error (both of the store's error types) and anything else crashes. Panels are QFileDialogs and alerts QMessageBoxes shown with open() over window (a QPointer<QWidget>), so nothing blocks; without a window they stand alone, where Swift runs the alert modally. A project is a folder: the open panel is in Directory mode and takes one project (no QFileDialog mode takes several folders, as Swift's workspace panel takes several packages); the save panel is a file panel with the comp suffix, and a typed name that is an existing project enters that folder, as any save dialog does (probed; GTK's does too), so Save As cannot land on an existing project. An accepted panel always holds a choice, read as selectedFiles().value(0). A chosen name that does not end in comp (in any case) gets .comp appended, where NSSavePanel enforces the project type: setDefaultSuffix only fills in a missing suffix. The panel never saw that completed name, so if anything exists there (a link that leads nowhere counts) the save is refused with an alert and nothing is replaced. open trims the slashes at the end of its path, supplied or chosen in the panel: a shell completes a folder's name with a slash, and QFileInfo finds no name behind one, where Swift's lastPathComponent does. It trims only the end: QDir::cleanPath would also collapse link/.., which names another folder once link is a symbolic link. Callbacks that the session keeps (waitForFileRequest, importImages) hold the controller through a QPointer, because the session may outlive it; everything else hangs on the controller itself (connections, watchers, posted calls) and ends with it. The alert's roles carry the keys: Return saves, Escape cancels. Swift's session.projectURL = … is setProjectPath, which notifies; a save marks the saved revision before setProjectPath, whose signal then shows the saved state (12.13b: the write lands after the project is free again). samePlace compares canonicalFilePath()s where Swift compares resolvingSymlinksInPath(). There are no security scopes; recent projects are 12.14b's. receive keeps Swift's queue: requests drain one at a time from the event loop, each after waitForFileRequest; a project is known by its suffix in any case, a trailing slash aside. tests/DialogDesk.h answers the real dialogs from a timer: it types into a panel's name field (selectFile does nothing while that field has focus) and clicks or keys an alert. While a desk lives the working directory is a temporary folder of its own, so a panel never starts in the source tree (a mutant once saved Untitled.comp there and skewed the runs after it).
  • ProjectWorkspace is a QObject with one signal, changed(), as the session has: it mutates itself, so the rule for @Observable classes that only the session writes does not cover it. tabs, selectedID and isManaging are read through getters. A ProjectTab holds its session and controller by value, the session first, and lives in a std::shared_ptr, as Swift's tabs are references: a flow that awaits a tab's controller or session (a load, a close, a quit, an import, a copy) keeps that tab alive in its callback until the answer has run, as Swift's await does, so a tab removed meanwhile still ends its flow. Every callback the workspace hands to a tab's controller or session holds the workspace through a QPointer. Swift's async calls take completion callbacks posted to the event loop; close takes one too, because its tests await it. The open panel takes one project (see the controller) and is titled Open Project like the controller's, where Swift's bare NSOpenPanel says Open: Qt's default for a folder panel is Find Directory. Managing ends last in every flow, after the sessions are free again, so that the workspace's last signal sees canSwitch as the call leaves it. A title cuts the suffix as Swift's deletingPathExtension does (nameWithoutSuffix: a name's first or last dot cuts nothing, so .comp stays whole), where completeBaseName would leave nothing. samePlace falls back on the absolute path (absoluteFilePath, which also cleans it) when a place no longer resolves, as resolvingSymlinksInPath does, because two missing places both canonicalise to the empty string. A loaded tab's default name is never shown (its title comes from the session's path, which the controller trims), so loadProject names it from the path as given. receive looks again every 30 ms while the workspace cannot switch, as Swift sleeps; Task.isCancelled has no twin. receiveProviders takes the drag's QMimeData: the layer-row type carries a UUID, judged by its text so that all zeros stays a valid id (a payload under another type is no row and goes to ImageFileDrop since 10.4, which reads nothing there); files and pictures arrive with 10.4. copyLayers (since 12.10b; copyLayer hands it one id, and a drop still copies row by row, as Swift's) bakes every mask that comes from outside the copy in one worker task (Swift awaits them one by one), then places the layers from a friend's seat in the target session, because Swift writes that document in place; ProjectController::isProject and samePlace are public statics so that both classes read paths alike. The anchor is the first layer's centre, or with several the centre of the box round every copied layer that is no folder; the ?? centre fallback is dead (the first id is always among the copies) and is a checked read instead. The copies end up selected, the first active; an id the source tab lacks (two tabs of one project share ids, and the first holding the first id is the source) is left out of the selection, as Swift's compactMap.
  • CanvasView twins EditorCanvas.swift, split as EditorCanvas.cpp (the display) and EditorCanvas+Navigation.cpp (events, cursors, focus; keys in EditorCanvas+Keys.cpp since 12.2b). It follows Swift's updateNSView: ContentView calls consumeFocusRequest and synchronizeDisplay on every changed(), and a canvas alone follows nothing, as Swift's alone observes nothing. paintEvent clips to the dirty rect (the region's union, as draw(_:) receives it) and paints it through AdjustmentSurface::draw, so hand-blended modes and clip coverage meet an image-backed device and device-sized coverage costs only the dirty area; the smooth path draws its layers through a second surface, Swift's transparency layer, so blend modes never meet the checkerboard. DisplayState holds what Swift's holds (brushRevision came with 8.2, adjustments with 9.5, effects with 9.6c, the crop union in renderBounds with 10.1); with a live mask every layer counts, hidden ones included, since a hidden source still clips. The surround outside the document is the palette's Base, which the built-in dark sets to Swift's 0.105 gray and a light Omarchy theme to its light darker_background, so the welcome reads over it. The shadow is fourteen rings of black stacking to 35% at the edge (Qt has no blur); the border is antialiased, half in and half out, as CoreGraphics strokes it. A wheel without pixel deltas is read in lines, QApplication::wheelScrollLines() a notch, twelve points a line as Swift's non-precise scroll; Qt::ZoomNativeGesture is magnify. Geometry reaches the viewport from the event loop (syncGeometry is posted, as Swift defers past SwiftUI's layout) and notifies the session; QWindow::screenChanged stands in for viewDidChangeBackingProperties. A shown canvas takes the keys when a document exists and no sheet, importer or modal dialog holds them; EditorSession::requestCanvasFocus counts Swift's canvasFocusRequest, NavigationToolHeader asks on Return and Escape, and ContentView posts one when a document appears (Swift's onChange). Cursors: the open and closed hands for Hand and Space, a drawn magnifier for Zoom (Alt shows the minus), the I-beam for Type, the arrow for Idle and Move (the transform cursors arrive with 6.1), the cross for the rest; auto-repeat releases of Space and Alt are no release. Keys: the tool letters, Space, Escape and Return around a transform edit, arrows nudging in Move; the rest arrive with their tools. Swift's local monitors (viewDidMoveToWindow) are an application event filter installed while the canvas is shown: Shift-+ and Shift-− step the blend mode from any control of the window but a text field (QLineEdit) or the canvas holding open text (Swift's monitor stands aside for any NSText first responder, the inline text view included; since 8.9b), and Alt reaches the Zoom cursor from any window, an open menu's included (Swift's modifier monitor is application-wide); a blend key from another window, a dialog included, is that window's own.
  • BrushRaster::visibleRect and AdjustmentSurface::draw work on widget painters as on images: a widget's QPainter::device() measures itself in logical pixels where an image's measures device pixels, combinedTransform() carries the device pixel ratio, and deviceTransform() also the widget's offset in its window's backing store, whose image is the engine's paintDevice(). HighDpiCanvasTests holds this at ratio 2 through a main of its own that sets QT_SCALE_FACTOR.
  • EditorSession::drawLiveComposite lives in LiveLayerMask.cpp, as in Swift, and draws the document as the export does, in document pixels, through the displayed transforms, placements and blend modes; LayerMaskSessionTests compares it with the export on every render.
  • ContentView is one project's editor area, a QWidget that compares what it shows with what the session holds on every changed(); the window around it is ProjectWorkspaceView (4.3c), which owns the toolbar that SwiftUI lets ContentView declare. ToolHeaderBar carries Q_OBJECT: Qt 6.11's findChild refuses a class without one at compile time. Every tool has a bar of ToolHeaderStyle::height, so the canvas never jumps: a tool whose bar has not arrived yet shows an empty ToolHeaderBar, the idle and Eyedropper bars are titles alone, Hand and Zoom share one NavigationToolHeader, and a bar put in late is shown at once (a layout shows a late child only from the event loop). The welcome is made anew each time the document goes, as Swift's if document == nil makes a fresh view, so its fields and its clipboard suggestion start over. A rail button is a QToolButton that toggles itself on a click before the session answers, so the click's slot sets every button again from the session, which a refused selectTool leaves silent. NewCanvasSheet takes Return and Enter from anywhere on the sheet through widget shortcuts, as SwiftUI's .defaultAction does: setDefault alone reaches only a QDialog. Fonts are set in pixels, Swift's points on a display of one: setPointSize would grow them by the screen's DPI. Swift's .secondary text is the palette's PlaceholderText, its orange warning the palette's BrightText, which the theme of 4.5 fills; dividers are Mid. Labels and hints say Ctrl and Alt for ⌘ and ⌥. The status bar's hint is an eliding label and the one item that gives way when the window is narrow, as SwiftUI truncates it; zoom and size print through the English locale, as Swift's interpolation groups digits (1,200 × 800 px). Swift's last branch gives the Eyedropper the Zoom tool's hint; the twin says the same. Hints and icons that follow tool state (marquee and lasso kinds, brush and smear modes, shape kinds) arrive with that state. NavigationToolHeader applies on Return and Escape directly and again when focus leaves, where Swift applies only on the loss of focus: a Qt widget in a window that is not active loses no focus. Text that is no number reads as zero through toDouble, which the > 0 test refuses, so no ok flag is read; inf reads as a number and the viewport refuses it. NewCanvasSheet::clipboardDimensions reads a copied picture's size from its header through QImageReader (no pixel is decoded), PNG before TIFF as in Swift, sides swapped for a stored quarter turn. The importer is a QFileDialog for existing files, and the two alerts are QMessageBoxes opened over the view; each is taken down again when the session's flag or error is cleared from elsewhere, as a SwiftUI binding would dismiss it.
  • LayersPanel twins LayersPanel.swift: the heading with its count, LayerAppearanceControls, the rows or the empty words, and the footer's QToolButtons (new layer, folder, LayerMaskMenu, the effects menu since 9.6d, the adjustment menu since 9.5d, delete). Its width is ContentView's layersPanelWidth, kept in QSettings under Swift's AppStorage key; a stored width outside minimumWidth…maximumWidth is the default, logged. PanelResizeEdge lives in ContentView.cpp as in Swift: an 8-point grip with the split cursor over a Mid hairline, dragged left to widen, whole points, within range. Tests enable QStandardPaths::setTestModeEnabled, which sends QSettings to ~/.qttest. The delete button is Swift's deleteLayerOrMask (a chosen effect goes first since 9.6a, then a selected mask, then the selection; since 9.6d its words name a chosen effect first).
  • NativeLayerList twins the NSTableView with a QScrollArea of LayerCells, one a row, made anew only when the ids change (a selection or rename reconfigures the cells in place, Swift's "no reload" rule; nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity compares the cell pointers) and shown at once, since a layout shows late children from the event loop. Each cell lays its controls out by hand from Swift's constraints (eye, disclosure, thumbnail, mask link, mask thumbnail, name, size, the effect rows since 9.6e; the depth and a clipping mask each step 24 points in) and keeps a ThumbnailKey (asset identity, transform, canvas size) so a picture is redrawn only when what it shows changes. EyeSwipeButton and LayerThumbnailButton are QToolButtons that answer the press themselves. The eye begins a swipe and sets the rows under the drag, scrolling the list by the pointer's overshoot past the viewport as autoscroll(with:) does; Swift tracks the drag inside mouseDown with a defer, so the twin watches the left button's release application-wide (a menu opened mid-press takes it; another button's release leaves the swipe) and a row rebuilt mid-swipe takes the button with it, so its destructor ends the swipe from the event loop, never inside the rebuild that deletes it. The thumbnail follows Swift's mouseDown order: Alt on a mask chooses the mask on a release without a drag, Shift toggling it as selectMask does (the copy's drag arrives with 4.7), Alt elsewhere is the row's, Ctrl waits for 7.1's selection loading, Shift on a mask targets and toggles it, a plain press targets the pixels or the mask, and every press but Ctrl's then goes to the row with its modifiers, so Shift ranges from the active row without targeting first. A double click with a modifier is a second press of the button's own (Swift's mouseDown runs twice), and Alt's on the row's strip clips again; a plain one is left to the row, which renames as Swift's renameClickedLayer does for a pixel layer (text opens since 8.9a, an adjustment's settings since 9.5d). QTest::mouseDClick on a widget sends the double click alone; the window overload sends the press, release, double click and release a real click brings. The row's own press is clickRow (it first lets go of a chosen effect since 9.6e; Alt in the bottom 10 points of the whole row clips to the layer below, Ctrl extends, Shift ranges from the active row, a name click on a lone mask-selected layer commits and targets the pixels, a press on a selected row keeps the selection). A rename is typed in the row's QLineEdit (Return keeps, Escape cancels, leaving keeps) and the session's renamingLayerID is cleared before the rename, because the rename's own signal would reopen the editor; then the list takes focus, as Swift's window.makeFirstResponder(table). A focus lost to the menu bar or a popup (Qt::MenuBarFocusReason, Qt::PopupFocusReason) is no leaving, for the editor and the opacity field alike: an NSMenu never ends editing. The name and size labels are Qt::PlainText, so a name is never read as markup. The context menu is the list's since 12.10a (below). Hidden rows fade through a QGraphicsOpacityEffect. The list's keys mirror the canvas's (tool letters, Escape and Return around a transform edit, arrows nudging under Move, where every edit sits) and Up and Down walk the rows otherwise, Shift ranging from the anchor as the table's (the far end of the selected run around the active row; the range shrinks back towards it), the row brought into view. A palette change redraws every icon from the event loop, once the widgets carry the new palette: the list's update, the panel's and the mask button's icons; a folder's icon keeps its ink in its ThumbnailKey. The canvas's application event filter already takes Shift-+ and Shift-− from the list; BlendShortcutTests holds both and the text-field exception.
  • Drags in the list (NativeLayerList+Drag.cpp): a row pressed and moved past startDragDistance starts a QDrag carrying the row's id, or its whole selection's, one id a line under ProjectWorkspace::layerType (Swift writes one pasteboard item a row); Alt at the press offers CopyAction alone under CanvasView::duplicateCursor, as the table's source mask does, and the drop then duplicates. dropTarget is Swift's validateDrop: a folder's middle half takes the drop in, the rest lands above the nearer row edge or below every row, every row must pass canPlaceLayer (a copied folder carries its contents since 12.2a); acceptDrop is acceptDrop, the place worked out once, the order reversed into a folder, the moved rows kept selected, one Move Layer(s) or Duplicate Layer(s) step. Swift refuses a drag from another table by its source; the twin's drag carries its list's token under sourceType and a drop takes only its own (Qt's synthetic drop events carry no source(), and two projects opened from one file share their layers' ids, so neither would do). A press arms the drag with its modifiers, on the row or on a thumbnail (a thumbnail press that targets goes on to arm the row, as Swift's mouseDown falls through to the table); every left press first drops whatever an earlier press armed, then arms unless it clipped (Swift returns after clipping) or was Ctrl's or Shift's; only a move with the left button held past startDragDistance starts the drag. An Alt press on a mask watches the left button's release application-wide (a popup opened mid-press takes it), as the eye does; a release seen on a QWindow is not counted, since the window sees the button's own release before the button does, and another button's release leaves the press, as Swift waits for the left one. A plain drag offers Move and Copy with Move preferred, so a project target can take the Copy Swift proposes (a tab, the canvas and the New button set CopyAction on a valid enter, move and drop and ignore the rest); the list itself duplicates only when a copy is offered alone (Alt at the press), whatever Ctrl makes Qt propose, and sets Move or Copy itself. A drag held near the viewport's edge scrolls the list by the overshoot into a 16-point margin and keeps scrolling on a timer until the pointer moves back inside or the drag ends (a leave or a drop stops it, so no tick reads a drag that is gone), as the table autoscrolls (Wayland sends no moves while the pointer holds still); autoscrolling() says whether the timer runs, for the tests. An Alt click that clips sets the cursor for the next click at once, as Swift's refresh after toggling. LayerColumn paints the indicator: a two-point accent line between rows or the accent-filled folder row. A mask's Alt-drag carries its layer's id under maskType, read back through ProjectWorkspace::layerID (a payload that is no id names no mask, an all-zero id included), and lands on the row under the pointer through canCopyMask. QDrag::exec needs the platform's drag loop, which offscreen has not: LayerDragGestureTests defines both QDrag::exec overloads in the test executable, where they take libQt6Gui's place (as ProjectReplaceTests does with libc), record what the drag offered and return IgnoreAction, so presses, thresholds and actions are tested to the call; LayerDragTests holds the drops and LayerDragFixtures.h what both share. The eye and thumbnail buttons live in NativeLayerList+Buttons.cpp. With Alt held the viewport's cursor follows the pointer (cursorFor): the clipping cursor over a row's strip (create or release, drawn as paths like the symbols, refused where canToggleClippingMask says no), the duplicate cursor over the rest of a row (a folder's strip included, since a folder clips nothing, 12.2a) and over a mask thumbnail, the arrow otherwise and with Ctrl; Alt or Ctrl pressed anywhere, and hover over any row or control (every cell and its children track the mouse), reach an application event filter installed while the list is shown, as Swift's flagsChanged monitor and tracking area; the list keeps the last hover point, so a modifier change is judged where the pointer sits (QCursor::pos is no witness offscreen), and a hover's own modifiers are the event's.
  • EditorSession::duplicateActiveLayer and duplicateLayer (Document/SelectionClipboard.cpp, the rest of that extension arrives with 7.4) came forward from 5.1 for the Alt-drag: the copy is the layer with a new id and " copy" after its name, inserted above it (insertCopy); since 12.10b duplicateActiveLayer is duplicateLayers of every selected layer (none inside a selected folder, in document order: copiedLayerIDs; Swift's union with the active layer is dead and left out, since selectLayers always keeps the active layer selected), one step, where several copies stack together above the topmost original in the panel's order, in its parent, and end up selected, the active one's copy active; since 12.2a a folder copies with every layer inside it, new ids throughout, parents and clipping links inside the copy pointing inside it and those outside kept, a collapsed folder's copy collapsed, and nothing past 10,000 layers; duplicateLayer selects, duplicates and places the copy inside one transaction, refusing a place canPlaceLayer refuses (which reads canEditLayers, Swift's first term) and answering false when the limit made no copy.
  • LayerMerge.cpp, LayerFlip.cpp and Filters.cpp twin the Swift files of those names (Filters.cpp holds PixelFilter::trimmed alone until 9.x). trimmed returns a Trimmed struct where Swift returns a tuple, and reads the alpha bounds straight from a premultiplied RGBA8888 image, drawing any other format into one first, where Swift always draws into a context. mergePlan is a private MergePlan struct. mergeLayers renders through drawLiveComposite into a document-sized context and takes the trimmed image's own size through ImageLayer(asset, origin), where Swift also assigns trimmed.transform: at 1:1 the two are equal by construction. Its insertion index counts the kept layers before the anchor, so Swift's ?? layers.count and the clamp are dead and left out; the hierarchy check stays (a folder selected together with its own child would put the result in a removed folder), and Swift's beeps are warnings on omaphoto.rendering, as is a context that cannot be allocated (Swift returns in silence). flipLayers reads the group box and the active layer through value(), since canTransform has proved both, and drops Swift's ids.isEmpty guard for the same reason; commitTransform has already run finishOpacityEdit, so Swift's second call in mergeLayers and flipLayers is left out. LayerTransform::mirrored is declared in LayerTransform.h and defined in LayerFlip.cpp, as Swift's extension. beginDuplicateTransform keeps a private TransformDuplicate; a selected folder carries its selected contents, which are no targets of their own, and since the layer limit can refuse a copy, Swift's copies.isEmpty branch (the step closed, the selection back) is kept (12.2a; the copies are found by the selection since 12.10b); a cancel removes each copy with its contents and their collapsed marks. commitTransform ends the duplicate after its own transaction (endDuplicateTransform, Swift's defer) on the layer path only: selectLayers has just made a copy active, so no mask is selected and the mask path never carries one. cancelTransform removes the copies, restores the selection and ends it. The Layer menu's Duplicate Layer entry calls duplicateActiveLayer, what Swift's layerViaCopy does without a selection (7.4 brings the selection); Merge shows mergeTitle and Flip Layer follows canTransform.
  • The Move tool on the canvas (6.1): TransformOverlay is no widget. Swift's overlay view exists so that a selection never redraws the pixels; a transparent Qt child would repaint the canvas beneath it anyway, so the canvas draws the handles and guides itself, after its pixels, and synchronizeDisplay repaints only the union of the old and the new drawnRect (a guide spans the canvas) when the geometry or snapGuides changed, leaving its return value to the pixel state. TransformOverlayGeometry keeps Swift's handles, rotation grip and hit; showsRotation and the corner constructor arrive with 6.2. resizeCursor maps Swift's frame positions onto Qt's four resize shapes (right, bottom right, bottom, top right). EditorCanvas+Transform.cpp holds the drags (beginTransformDrag, dragTransform, endTransformDrag, cancelTransformDrag, Swift's resignFirstResponder), transformPressLayer and transformCursor; Ctrl stands for Cmd in picking, Ctrl+Shift extends the selection. The drag's cursor is kept in m_dragCursor until the release and outranks Space, where Swift disables the window's cursor rects; hover reaches updateCursor through mouse tracking and the last hover point, the application filter reads Alt and Ctrl as it read Alt, and a shown canvas reads both from the keyboard. The release synchronizes the display, so a Ctrl+T edit's guides go without a commit. moveCursor (the arrow with Swift's four-way badge) and rotationCursor (two arrows round a ring, for the SF symbol) are drawn as paths; the distort cursor arrives with 6.2. SnapGuides is a struct where Swift has a tuple, a public member like viewport, unannounced; commitTransform and cancelTransform clear it. transformSnapTargets and snappedMove live in Document/Crop.cpp, Swift's file. TransformEdit.persistent sits last, so the positional initialisers keep their order; a drag's edit and a duplicate's are not persistent. transformAutoSelect, showsTransformControls and locksTransformRatio are getters and setters that notify. Escape and Return end a drag with its edit and put the hover cursor back. A press that never moves commits an unchanged edit, which the history drops. The pixel tests hide the controls (CanvasDisplayTests, HighDpiCanvasTests) and preview through a drag's edit, since Swift's overlay never reaches its draw tests.
  • Distort (6.2): DistortWarp keeps Swift's tests of usability and convexity, homography (a projective QTransform where Swift returns a closure, Qt dividing by the third row as Swift by w), warp, warpTrimmed, carried and warpMask. Core Image's perspective filter is QTransform::quadToQuad on the image's pixel rect and a drawImage under it, antialiased and smooth; a folded shape is two triangles under their affine maps, each a hard device-pixel region of the pixels whose centres the triangle holds, built per scanline from each edge's ends ordered by y, the left bound inclusive, so the halves partition the diagonal and meet a fractional shared edge with one x (a path clip turns soft once the image draws antialiased, and QRegion's integer polygons round the vertices), copied through BrushRaster::draw without interpolation, as Swift's. In distortPreview a mask warp that throws is caught on its own, as Swift's try?: a linked mask covering its layer then draws none, a placed one falls back, with a folded shape, to its stored placement clipped over the warp. Its Warped and Trimmed structs stand for Swift's tuples; mapPath arrived with 8.2b and distortedEffects arrived with 9.6c. Swift's DistortTests agree since 1.2.1 (7111885): a bow tie is usable and folds as two triangles, not convex, and a collapsed corner is refused by both tests; the preview takes a folded shape. TransformEdit.corners sits last, after persistent. The session's distortPreview and maskDistortPreview are const with mutable caches (DistortPreviewCache by layer, MaskDistortPreviewCache), keyed by ImageIdentity where Swift compares CGImage identity, because the canvas reads them from a const session; a warp that throws leaves a cached empty result, as Swift's try?. commitDistort catches ProjectError and ExportError into brushError as Swift catches its errors; commitTransform runs it after the mask path, before the plain one, and ends a duplicate after it. commitMaskTransform takes the corners path with Distort Layer Mask, displayedMaskPlacement its corners cases, nudgeLayer moves the corners with the draft. The overlay's corners constructor sets showsRotation false, hit skips the grip then, and the drawing leaves the stem and disc out; geometry returns the corners' geometry for a group or the edited layer. The canvas's DisplayState carries the edit's corners, so a moved corner repaints; drawOwn draws distortPreview first and takes maskDistortPreview for the mask. A Ctrl-drag on a handle begins the distortion and drags in distort mode with the white distortCursor; the cursor shows it over a handle under Ctrl or once distorted. The inspector's numbers rest while corners are set, as Swift's .disabled.
  • TransformInspector is the Move tool's ToolHeaderBar: Auto Select and Show Controls as QCheckBoxes, the numbers in a QScrollArea without bars (Swift's horizontal ScrollView, which takes the row's spare room and keeps its content at its own size: the row's end stretch rests, the content ends in a stretch of its own), the ratio lock a checkable QToolButton with the panel's link icon, the sampling a QComboBox, Flip H and V and Cancel and Apply as QPushButtons. TransformValueField applies as typed (Swift's isFinite test is left out: previewTransform refuses every transform an infinite number makes), shows the session's number while unfocused and puts it back on FocusOut, steps with Up and Down (Shift ten) writing the number it applied, keeps its typing while a menu or popup borrows the focus (MenuBarFocusReason, PopupFocusReason), and on Return or Escape hands the canvas the keys as Swift's onSubmit and onExitCommand; the pending edit waits there for the canvas's Return or Escape. From any other control of the bar, Return applies and Escape cancels, Swift's default and cancel actions. The lock's icon is redrawn on PaletteChange, as the panel's. Swift's .id(activeLayerID) remake is synchronize clearing any focus of its own when the active layer changes. resizeBox is Swift's resize, whose name would hide QWidget::resize; its number >= 1 guard and the scale's number > 0 are left out: a side under a pixel is a box previewTransform refuses. The Layer menu's Transform Layer entry calls beginTransform (Swift's transformCommand does the same without a selection; 7.4 brings the selection) and the View menu's Show Transform Controls is checkable under Ctrl+H, enabled in the Move tool with a document.
  • Selections (7.1): DocumentSelection holds a QPainterPath where Swift holds a CGPath, SelectionClip a QImage coverage; SelectionClip::apply has no twin, because QPainter has no soft clip: PixelAdjust::coverage draws the clip's coverage through the layer's inverse pixelToDocument into a mask surface (smooth, as CoreGraphics filters a mask clip) and PixelAdjust::blend mixes two images through it byte for byte, rounded, where Swift runs CIBlendWithMask. Qt's united, subtracted and intersected honour each path's own fill rule and return odd-even paths whose fill is the region they name (probed, QPathClipper), so an outline made here is WindingFill (a lasso crossing itself fills like Swift's .winding, a traced mask keeps its holes) and a result keeps the rule Qt gave it; every fill and contains reads the path's own rule, and QTransform::map keeps it. Expand and Contract stroke the outline with QPainterPathStroker (round cap and join, Swift's copy(strokingWithWidth:); Swift's miter limit is idle under a round join and left out), and a contraction past the middle leaves Qt's empty path, isEmpty. DragBox::rect rounds with std::round, Swift's rounded(). The session's selection state is private with getters and setters (lassoKind, marqueeKind, selectionModeChoice, selectionAntialiased, the two amounts), lassoDraft and heldSelectionMode getters alone; selectTool cancels a lasso, as Swift's does. pressMarqueeKey and pressLassoKey arrive with the canvas keys of 7.2; displayedSelection with the ants. PixelInvert::run is a byte loop where Swift calls vImage (alpha − colour keeps premultiplied transparency), reading any other format as premultiplied first; invertPixels takes a completion callback, runs the job through QtConcurrent::run and a QFutureWatcher with the project busy, and writes back only when the layer still holds the pixels it computed from (identities compared, Swift's ===); the worker catches ExportError alone, the UI side std::runtime_error (the clip, the expanded 1×1 mask, the thumbnail), each a warning on omaphoto.app and brushError. invertPixels moves brushRevision after its step, as Swift's does. addMask from a selection draws the outline through the inverse of pixelToDocument into the layer's own grid; a blank layer's grid is its rounded size, as Swift's. deleteLayerOrMask is the session function the panel's trash, the Layer menu's Delete and (from 8.x) the Delete key share. The loaders in MaskTracing.cpp warn on omaphoto.app where Swift beeps, and fetch the pixels inside a try: a painted asset flattens on first use, and a flatten that cannot allocate loads nothing, where Swift's CGImage already exists. The Select menu and the Image menu's Invert sit between View and Layer, as macOS places CommandMenus; Select All in a text field is the field's, like Undo (QLineEdit takes Ctrl+A itself through ShortcutOverride, so the entry's own branch matters only when triggered); Swift agrees since 1.2.0 (90ee29f, the responder chain first), and the entry never rests, as Swift's: without a canvas or a field it does nothing. The focus takes it first: a text field, open text, then a focused layer list, which selects every row with the top one primary, as NSTableView's selectAll(_:) does in Swift's chain (NativeLayerList::selectAllRows), and only then the canvas. The panel's Ctrl-click on a thumbnail loads it (Ctrl-Shift adds, Ctrl-Alt subtracts, as Swift's loadMode), the row untouched; with Ctrl held over an enabled thumbnail the list shows CanvasView::loadSelectionCursor, a pointing hand drawn as paths with Swift's dashed badge, since QCursor::pixmap is empty for a shape cursor. LayerMaskMenu calls addMask and says in its tooltip what the selection becomes.
  • The Marquee and the Lasso on the canvas (7.2): Swift's lasso gestures live in EditorCanvas+Selection.cpp, the selection cursors in EditorCanvas+Cursors.cpp. The canvas tracks Shift as it tracks Alt and Ctrl (the application filter sees the key from any window, as Swift's flagsChanged monitor), and a key change reshapes a Marquee draft and the cursor's badge at once. A press with a selection tool is lassoMouseDown; a move is moveSelectionTool (a hover moves a polygonal draft's rubber band and refreshes the held keys, as Swift's mouseMoved); a release is releaseSelectionTool; the draft's keys and the outline's nudges are selectionKey, ahead of every other key; losing focus is endSelectionGestures. A double click closes a polygonal outline from mouseDoubleClickEvent, since Qt delivers the second press there (its first press adds the corner, as Swift's first mouseDown does), never while Space pans. A held M or L's repeats are swallowed, as Swift ignores them, so a tool chosen meanwhile stays. An outline drag keeps moveSelectionCursor under Space, as the transform drag keeps its own. The ants are a 120 ms QTimer stepping TransformOverlay::antsPhase, repainting selectionRect alone (the outline's own rect, not the handles beside it); drawnRect spans the handles, the outline and the draft, the last two stroked antialiased with butt caps (Qt::FlatCap, as CoreGraphics dashes: Qt's square caps would stretch each dash by the pen's width) as CoreGraphics strokes them (a 1-point line on whole coordinates blurs over two rows at 1×, on macOS too); synchronizeDisplay repaints the overlay when the displayed selection or draft changes, refreshes the overlay's cached rect with every full repaint (a pan moves the outline) and refreshes the cursor when a kind or the displayed mode changes. The two timers are named children of the canvas (antsTimer, marqueeAutoscroll), as the session's busy timer is, so tests read whether they run. Autoscroll is a 16 ms QTimer with Swift's speed curve; each step translates the viewport, drags the draft or the outline again under the still pointer, notifies and synchronizes. moveSelectionCursor, selectionCursor and wandCursor are drawn as paths: the crosshair, the icon haloed by eight white offsets and the badge, and the wand with Swift's own strokes (hot spot 7, 7 at the sparkle, the stick down right, the badge at 17, 5), since QCursor::pixmap is empty for a shape cursor; the icons are Rendering/SelectionIcons, which the rail's ToolIcons and the cursors share, so nothing in Rendering/ includes UI/. SelectionIcon is that header's enum, not nested in CanvasView. Ctrl over the selection shows the move pointer until 7.4 brings the scissors; the wand's click and cursor are 7.3's. pressMarqueeKey and pressLassoKey choose their tools and nothing more, as Swift's functions do: Swift's tests once expected a second press to switch the kind, which no Swift code does (its cycleToolMode on Tab does, 8.x); since 1.2.0 (b5cb4fa) they test the code's rule, as the twin does (…AndKeepsTheShapeLastSet), and the twin's tooltips name no key. LassoControls is the selection tools' ToolHeaderBar: Swift's segmented pickers are checkable QToolButtons in a QButtonGroup, the amounts are SelectionAmountFields (a QLineEdit taking digits alone, every typed value clamped to 1–500 as Swift's binding clamps it, stepped by arrows within the bounds, Return and Escape handing the canvas the keys, a menu's borrowed focus keeping the typing as TransformValueField does), the wand's controls arrive with 7.3. ContentView::hint takes a ToolIconKind for the ellipse Marquee's and the polygonal Lasso's words, and the rail's buttons repaint with the kind.
  • The Magic Wand (7.3): WandPixels.c compiles in place beside the other kernels, and Document/MagicWand.cpp wraps it as Swift's MagicWand does: select draws the image into a premultiplied RGBA8888 context (the kernel's byte order, rows top down), floors the point, clamps the tolerance and hands the mask to wand_mask; outline frees the kernel's arrays through unique_ptr and builds a WindingFill path from its loops. MagicWandError carries Swift's two messages (what()), tooDetailed for the kernel's −2 and memory for −1. The session's magicWand takes a completion callback and runs the job through QtConcurrent::run and a QFutureWatcher with the project busy, like the invert; the worker catches MagicWandError and ExportError, and the UI side applies the outline only while the same document is open (its id, as Swift compares), New with no match deselecting, New with a match setSelection without the canvas clip (Swift's shortcut), the other modes applySelection. wandSample draws the composite through drawLiveComposite or the active layer's own pixels through LayerRenderer::draw at 1:1, a folder or blank layer as clear; a context that cannot be allocated logs and refuses, as Swift's try? returns nil. wandSettings is a struct with a setter that notifies; title and radius stand for Swift's computed properties of WandSampleSize. The canvas hands a wand press to magicWand with the press's mode, and a click inside the selection to magicWand in New mode where another tool would deselect. LassoControls shows the wand's controls with the wand alone: the tolerance is a SelectionAmountField bounded 0–255, the sample size a QComboBox of Swift's titles, the source two checkable buttons, Contiguous a QCheckBox; changeWand rewrites one field of the settings. The mutant that pins m_thisLayer checked is equivalent, since the exclusive group's later m_allLayers line decides, as with the marquee's kinds.
  • The clipboard (7.4): SelectionClipboard.cpp twins Swift's extension as far as the raster edits exist. PixelClipboard and CopiedPixels (Swift's tuple) live in Document/SelectionClipboard.h. renderSelectedPixels and renderMergedPixels draw into the copy region and multiply the selection's coverage in with DestinationIn through BrushRaster::alphaView (clipTo), where Swift clips the context (SelectionClip::apply has no twin); a mask copies as opaque gray with LayerMask::background for its ground when placed apart and LayerRenderer::drawCoverage for its white, each pass multiplied on its own (the white in a second surface), because CoreGraphics clips both and a soft edge then stacks: half coverage under a white mask is alpha 192, not 128. store gives the system clipboard the image and a PNG through QMimeData, and keeps a QPointer to the QMimeData it hands the clipboard where Swift reads the pasteboard's change count (since 12.10b; Qt's dataChanged is no count, since Wayland announces one copy twice): while this app owns the clipboard Qt returns that very object (offscreen, X11's client clipboard, Wayland's selection source), any other copy, a history manager's restore of the same bytes included, is another object, and a replaced object is deleted, which nulls the pointer, so a copy once replaced never counts again, as a change count never returns; a paste puts our pixels back in place while the clipboard still holds that object (clipboardHolds), else takes another app's image through QMimeData::imageData, converted to sRGB first when it carries another colour space (CoreGraphics converts as it draws; QPainter copies the numbers), then to the working format, tagged sRGB and centred. A cleared clipboard has no QMimeData at all, so canPaste checks the pointer. A working copy that cannot be allocated is a warning, as Swift's try? beeps there, where a failed Layer via Copy sets brushError as Swift's catch does. The clipboard is touched only by a copy or a paste, so session tests without one run under QTEST_GUILESS_MAIN still; SelectionClipboardTests runs under QTEST_MAIN. addPixelLayer's shape and text arrived with 8.8 and 8.9a. Swift's cutSelection, PixelMove and the whole FloatingSelection extension need clearSelectedPixels and the brush raster, so the cut arrived with 8.2 and the rest with 8.2b, where canEditLayers and ProjectWorkspace::canSwitch gained their pixelMove terms. The Edit menu's Copy and Paste check when chosen, as Swift's (a text field keeps its own, as with Select All); Copy Merged follows canCopyMerged; the Layer menu's Ctrl+J is layerViaCopy, titled by the selection and enabled as Swift's: canCopyPixels, or no selection on an editable layer (a folder too since 12.10b). Swift's beeps are warnings on omaphoto.app.
  • Content-Aware Fill (7.5): ContentFill.c compiles in place beside the other kernels; Document/ContentFill.cpp includes its header in angle brackets, because the twin's own header shares the name. ContentFill::run draws the job's image into a premultiplied context and takes the pixels to make from PixelAdjust::coverage (the selection through the inverse mapping, as Swift clips the mask context and fills white), the kernel's 0 is ContentFillError(noSource) with Swift's message and its −1 ExportError(render). Document/Filters.h holds what Swift's Filters.swift holds as far as it exists: FilterKind (rawValue and isAutomatic are free functions; the other kinds came with 9.4a), FilterJob (its settings, scale and seed came with 9.4a), PixelFilter::run (the kind's filter, then PixelAdjust::blend through the selection) and FilterEdit, a plain class in a std::optional of the session, since only the session writes it; its id stands for Swift's object identity, so a preview lands on the edit it was made for. FilterEdit grows through grow (the blur's growth and the preview copy came with 9.4a). The session's extension is Filters+Session.cpp: Swift's file holds 473 lines, its twin's two halves about 500. canContentAwareFill without its isMaskSelected, filterEdit and isEmpty terms (canAdjustColors holds all three; it sits in HueSaturation.cpp, the twin of its Swift file, alone until 9.3, with the terms that exist), beginFilter (cancelCrop waits for the crop), updateFilter (its settings came with 9.4a), cancelFilter, commitFilter with a completion callback, finishFilterCommit (Swift's guards, the mask carried through LayerMask::clipImage). Swift's previewTask is one QFutureWatcher: renderFilterPreview marks the edit preparing and starts the worker, or leaves the job pending behind the one running and runs it when that finishes, a run counting as running until its finish has run (m_filterRendering, as Levels' queue); a commit that awaits the preview (isAutomatic) is kept in m_filterCommitWaiting with its edit's id and called again from finishFilterPreview (every waiter is, and one whose edit has gone just returns); the wait is on preparing, which only the delivery clears; Preview off is 9.4a's; the workers catch ContentFillError and ExportError alone. brushRevision moves where Swift's does (a delivered preview, Preview switched either way, a cancel, a commit's end), so the canvas's DisplayState sees each new preview. displayedTransform and displayedMaskPlacement take the grown transform while the edit previews, and the canvas draws previewImage in the layer's place. canEditLayers and ProjectWorkspace::canSwitch read filterEdit. FilterSheet (a filterPanel's content since 9.4b): the kind's words, Preview, the error in BrightText, "Limited to the selection" while a selection exists (never for an adjustment layer's editor, since 9.5d), Cancel, the spinner with "Working…" or "Applying…", and OK as the default button, disabled while an automatic kind prepares or failed, so Return then does nothing; Escape and the close button cancel, as Swift's cancelAction and onClose. The Edit menu's Content-Aware Fill… is Shift+Backspace (Swift's .delete is that key) and follows canContentAwareFill; the canvas takes Shift with Backspace or Delete ahead of the selection keys, as Swift's keyDown does.
  • The brush on the CPU (8.1): Rendering/BrushCoverage twins MetalBrushCoverage: the shader's maths in float, pixel for pixel (the centre sample, the hard tip's antialiased silhouette over max, the soft tip's optical density under Gauss–Legendre quadrature per segment at the deposition spacing, the 20 cap, the permanent paint apart from the replaceable tail), one band of 32 rows a QtConcurrent task where Metal dispatched one tile a thread group. A Tile keeps the permanent density alone: Swift's preview buffer is the gray coverage context itself, written row by row, so nothing is copied back. A tile that cannot be allocated is ExportError(render). The class with its shared device is a namespace. Document/BrushStroke keeps Swift's continuous path alone: the dab fallback for machines without Metal (walk, dab, tip, stamp, gridTip, falloff, drawTail, removeTail, curve, tailBackup, distanceToNext, dirtyTiles) is dead where the CPU coverage always exists, and spacingFraction stays because the integral reads it. BrushStroke.swift runs to 899 lines, so the twin splits: BrushStroke.cpp (BrushRaster, with fill, source-over of a colour at coverage × clip × alpha byte by byte on RGBA and gray alike, 1:1 by construction), BrushStroke+Stroke.cpp (the grid, samples, the centripetal Catmull–Rom subdivided within 0.2 px, keys, tiles, coverage and publish) and BrushStroke+Commit.cpp (transform(for:), paintSnapshot, commitInput, BrushCommit as a namespace, since Swift's actor only serialises). The grid is worked out first (Grid) so the const members can follow Swift's lets; Swift's guards throw ProjectError(tooLarge) as one, and the extent is judged as a double before it becomes an int, because past int the conversion is undefined where Swift's Int just holds the number. The stroke's source is the asset itself (m_source, an ImportedImage), so a painted layer draws from its tiles and never flattens; commitInput hands the asset on, and BrushCommit::render draws it the same way. A selection clips the context in Swift; fill takes the selection's PixelAdjust::coverage on the tile's grid as its clip and rounds coverage × clip × alpha once, as CoreGraphics composites a clipped context. A commit whose crop cannot be allocated is ExportError(render), as Swift's cropping returning nil; so is a tile's context that cannot be copied on publish (QImage::detach leaves a null image where CoreGraphics' makeImage returns nil). Tile keys (row × columns + column) are qint64, Swift's Int: a small layer scaled to a point across a big canvas passes int. A tile is rebuilt whole on every publish (dirtyTiles only ever narrowed the dab fallback's rebuilds); base is the tile's first pixels, kept apart by QImage's copy on write. BrushSettings holds the tip and colour; erasing, healing and healingMode arrive with 8.2 and 8.4, selectionClip is here since publish reads it, clone, isBlur, replacesWithClone, editName, fillGradient, fill, clearPixels, paintCanvas, liftSelection, moveLifted and heal with their items. BrushIntersectionTests runs the one path where Swift loops over useGPU; BrushTests holds the stroke-level halves of Swift's session tests, driven on a BrushStroke made from a layer, with LayerRenderer::drawBrushPreview as the live view and BrushCommit::render as the commit; the session halves arrive with 8.2. The early return of a segment past the tip's radius is an equivalent mutant: the density there is zero anyway.
  • The session's brush (8.2): EditorSession+Brush.cpp twins Swift's extension as far as the tools exist: canPaint with the terms that exist (the adjustment term since 9.5a), makeRasterEdit (the budgets, the selection clip; pixelLimit is qint64, Swift's Int, and may run below zero when other layers use more than the budget), beginBrush for the Brush (Spot Healing joins with 8.4, the other brush tools with their items; erase and the mask's black or white as Swift sets them), continueBrush, shiftLineStart, cancelBrush, finishBrushImmediately (busy, it waits; otherwise it flushes and commits the snapshot), finishBrush (Swift's async wrapper is a plain call here), commitPaintSnapshot (the tiles land at once with their undo entry; a mask that covered the old grid is regrown through RasterSnapshot::replacing), commitRasterEdit (a completion callback, the render through QtConcurrent::run and a QFutureWatcher with the project busy, the worker catching ExportError alone, the UI side keeping Swift's guards and the mask's isEnabled as it stands at commit), usesOpacityKeys, typeOpacityDigit (Swift's uptime is std::chrono::steady_clock unless the caller passes a time, as the tests do), changeBrushHardness, changeBrushSize. brushStroke is a std::unique_ptr read through a pointer, since a stroke's const members forbid copying; brushSettings, brushMode (BrushToolMode in Document/SmudgeLiquify.h, whose smear and liquify arrive with 8.6) and maskPaintWhite are getters and setters that notify; lastBrushPoint and pendingOpacityDigit are private structs. brushRevision exists after all, Swift's counter, because the canvas's DisplayState must change with every step of a stroke, whose patches it does not hold; a step that changes nothing else repaints the stroke's dirtyDocumentRect alone, in view points two points out (strokeDirtyRect), as Swift's setNeedsDisplay does, and any other change repaints the view. While a folder's mask is painted, the clip for the layers inside comes from the edit's tiles (liveFolderMaskClip, Swift's liveMaskEdit): drawBrushPreview draws the old mask and the patches, nearest, into gray coverage the size of the device, hidden outside the mask's grid, and that multiplies into the folder coverage; the old mask, or a painted mask's base, is halved near the size drawn first, as Swift's displayImage does, so a fine mask keeps its shade while painted. The session's private structs (Decoded, Wanded, RasterMade and the rest) live in EditorSession+Jobs.h as SessionJobs and are aliased in the class, so the header stays under 500 lines. The gates (canInvert and canAdjustColors among them), selectLayer, selectLayers, selectTool and selectLayerTarget refuse while a stroke lasts, canStartProjectOperation too, and importImages finishes a stroke first, as Swift's do; the tips' parking by tool family arrived with 8.5. SelectionEdits.cpp gains canEditPixels, fillSelection (the palette's colours are black and white until 8.10's ColorPalette; on a mask the background is the opposite of maskPaintWhite, as Swift's paletteColor), clearSelectedPixels, deleteKeyPressed (its effects term since 9.6a) and applyPixelEdit; cutSelection sits in SelectionClipboard.cpp. BrushStroke gains erasing in its settings (BrushRaster::fill takes DestinationOut for it, Swift's blend mode on the context; a mask never erases, so that mode on a gray context is a logic_error), paintCanvas (the base redrawn, the body under the inverse mapping and a hard canvas clip, then PixelAdjust::blend through the selection where Swift clips), fill and clearPixels; placedMaskPreview is declared in BrushStroke.h and defined in LayerMask.cpp, as Swift's extension, and a preview that cannot be made logs on omaphoto.rendering where Swift's try? is silent. On the canvas a pixel stroke draws its whole grid placed by paintTransform through TiledLayerRenderer::drawStroke, a mask stroke through drawMaskStroke under the layer's transform, and a mask on its own placement draws the layer through placedMaskPreview; the mask's clip takes the stroke's owner as Swift's base does. Delete and Backspace without Ctrl, Alt or Meta are deleteKeyPressed on the canvas and, plain, in the layer list; Shift with them stays the fill's. The Edit menu gains Cut (Ctrl+X, a text field's own), Fill with Foreground Color (Alt+Backspace) and Fill with Background Color (Ctrl+Backspace), which in a text field delete a word or the line as Swift's deleteWordBackward and deleteToBeginningOfLine (the selection when there is one, else backwards from the caret, so nothing goes at the start) (reachable from the opacity field alone: a rename or an empty document disables them, as canEditPixels does), and Clear Selection Pixels. tests/BrushFixtures.h shares the fixtures; BrushSessionTests and PixelEditTests hold the session halves of Swift's BrushTests, RasterSnapshotSessionTests the five session cases, BrushCanvasTests the two canvasShift cases and the canvas's Delete key.
  • Moving and floating selected pixels (8.2b): PixelMove is a class in Document/SelectionEdits.h, Swift's final class. Its raster is a std::shared_ptr<BrushStroke>, because the canvas draws it while the commit runs, so commitRasterEdit takes a std::shared_ptr<const BrushStroke> (the brush hands its stroke over); its offset starts at zero, since a default QSizeF is −1 by −1. beginPixelMove, movePixels, finishPixelMove (a completion callback; the move stays set until the commit lands, so pixels and outline never show at the old place), cancelPixelMove and nudgePixels (Swift's beep is a warning on omaphoto.app) live in SelectionEdits.cpp; displayedSelection follows a pixel move and a floating edit. BrushStroke::liftSelection draws the source into the region, from its tiles when painted, and multiplies the selection's coverage in (PixelAdjust::coverage under DestinationIn) where Swift clips; moveLifted rebuilds each tile from its base, cuts the hole through the same coverage under DestinationOut (not for a copy) and places the lifted pixels on whole pixels: a landing between pixels (a scaled or turned layer) is resampled first by its fraction, bilinear and rows in parallel (BrushRaster::shiftedByFraction, shared with Clone Stamp), because QPainter snaps an unscaled image to whole pixels (probed on 6.4), where Swift draws it with .high. FloatingSelection.cpp twins Swift's file: FloatingTransform rides in TransformEdit.floating (last) as a std::shared_ptr<const FloatingTransform>, because the edit is copied freely and holds the whole document from before; canTransformSelection, transformCommand, beginSelectionTransform (a completion callback: the outer Transform Selection edit opens, clearSelectedPixels runs, and its callback inserts the Floating Selection layer), floatingSelectionTransform, mergeFloatingTransform, cancelFloatingTransform, and FloatingMerge::merge, a namespace with a Merged struct for Swift's tuple. The merge writes pixels, transform and mask into the source layer in place, where Swift rebuilds the layer from listed fields: the fields that list leaves out do not exist yet (TASKS.md, "Session hooks owed"). DistortWarp::mapPath came forward from 6.2's list: it walks the path's elements (Qt stores a quad as a cubic) and keeps the fill rule. beginPixelMove and canTransformSelection leave out Swift's pixelMove == nil, transformEdit == nil and !selection.isEmpty, which canPaint reads already (through canEditPixels). displayedMaskPlacement's floating == nil term is dead and left out: the floating layer is made without a mask, and the open edit blocks adding one; beginDistort edits the edit in place, so floating carries over. The session's private structs sit in SessionJobs, now a private base struct of EditorSession (the aliases went; MergePlan and TransformDuplicate joined it), so the header stays under 500 lines. On the canvas a Ctrl-press inside the selection with a selection tool lifts and moves its pixels, Ctrl-Alt copies them, under movePixelsCursor (the arrow with scissors drawn as paths and haloed, where Swift badges the SF symbol) or the duplicate cursor, kept under Space as every drag keeps its own; updateCursor picks the outline and pixel drags' cursors ahead of Space, so lassoCursor, its only caller's helper, checks neither (Swift's has other callers); Ctrl-arrows nudge the selected pixels in any tool, Shift ten, never with Alt or Meta (macOS's Option and Control), ahead of the Move tool's arrows; losing focus cancels a pixel drag; drawOwn draws the move's raster as a stroke, and a step repaints the whole view, as Swift's (only a brush stroke repaints its rect). The Layer menu's Ctrl+T is transformCommand, titled Transform Selection while canTransformSelection holds and enabled by either gate. canEditLayers, canInvert, canAdjustColors and ProjectWorkspace::canSwitch read pixelMove.
  • The brush on the canvas (8.3): EditorCanvas+Brush.cpp holds Swift's gestures. A press begins a stroke, Shift drawing on from shiftLineStart through the press point; a drag continues it, and Shift holds an axis that the first three pixels from its anchor settle (the press under Shift, else the last pixel reached), the pixel waiting at the anchor until then. Swift clears the axis when Shift is let go and again when a new anchor is set; the first is dead and left out. The release reaches its own point, then finishBrushImmediately commits. A busy project holds the stroke: a drag, the release, Escape and a lost focus leave it. A stroke goes on only under a drag with the left button, as Swift's mouseDragged (a hover after a busy release paints nothing), and wheels and pinches neither pan nor zoom while it lasts, as Swift's scrollWheel and magnify refuse. A right-drag with a brush tool, no stroke and no Space, sizes the tip from the press (two view points a pixel, rounded, 1 to 2000), or with Shift sets hardness over 200 points, each putting the other back; the circle stays at the press. BrushCursorOverlay is no widget, like TransformOverlay: the canvas draws the circle last, white 2.5 under black 1, and while hardness is dragged a dashed inner ring (four points on, three off, given in pen widths as Qt counts dashes); update returns the old and new rings three points out, as Swift's setNeedsDisplay. Swift's hardness > 0 is dead (a zero ellipse strokes nothing) and left out. The circle follows the pointer, hover included but not a pan (Swift's mouseDragged pans first), hides under Space, on leaving and on a lost focus, and shows with the brush tools alone. The canvas's event takes Tab before the focus chain (Shift-Tab stays the chain's) for cycleToolMode, which switches the Marquee's and the Lasso's shapes, the Brush's mode and Spot Healing's type (the other tools' modes arrive with them); B and E choose the Brush and its mode; a digit reaches typeOpacityDigit only where usesOpacityKeys holds, else it goes on to the parent, as Swift's super.keyDown; brackets step size and hardness, and the application filter takes them from any control but a text field unless the canvas has focus, so a focused canvas's keyPressEvent re-reads the modifiers as Swift's keyDown does. The layer list takes Tab and Shift-Tab (Swift's table cycles on both), B, E and digits alike. BrushControls is every brush tool's ToolHeaderBar: Paint and Erase in an exclusive group, size (1–2000), hardness and opacity as a slider in thousandths beside a whole-percent field (opacity from 1%), the sliders following the session under QSignalBlocker so nothing rounded is written back, the fields stepping with Up and Down from the session's exact value as Swift's arrowSteps does (a slider's 33.3% steps to 34.3%; SelectionAmountField takes Swift's value and change closures, and the Lasso and Wand fields round in change as theirs do, an equivalent mutant since their values are whole), and the mask's black-or-white picker and note while a mask is selected; the colour swatch arrived with 8.10a. ContentView+Status.cpp holds percent, iconKind and hint, the eraser's words and icon among them; iconKind checks its tool although every consumer ignores a kind that is not its own (an equivalent mutant). updateCursor and the modifier helpers moved to EditorCanvas+Cursors.cpp. ContentView's destructor stops listening first: ~QWidget closes a shown window after the members are gone, and the canvas's focus-out then notifies (cancelBrush bumps brushRevision, as Swift's does).
  • Brush speed (8.3's gate: 4K at a scale of 2, an 800 px soft brush, median frame at most 8 ms): 6.8–7.4 ms measured, from 47. The soft tip's density comes from a table of 8192 steps over d²/r² from h² to the rim, in radii because hardness alone shapes it, linear between steps; inside h the tip is solid, the table's first value, so a near-hard tip keeps every step across its fade (from 0.9995 on, a table over the whole radius missed the falloff by up to 46 levels). Once the table's steps are finer than float can tell apart ((1 − h²) / 8192 under float's epsilon, from about 0.9995), even that table misses by a few levels: there tipDensity evaluates the shader's own formula, float for float, for dabs and segments alike, its solid and empty ends returning at once what the formula yields, so only samples in the thin fade pay for exp and log. BrushCoverageTests holds dabs and strokes on both sides of the switch against the shader's own quadrature, replayed in float. The kernel keeps the shader's order of float operations to the letter (midpoint ± halfLength · node − projection, never regrouped): 8192 pixels along a near-hard stroke a regrouping moved a pixel three levels, and a reference written from the twin's own code repeated it. Its one read per lookup is checked at the upper step, so a lost bound aborts. Only a segment passing through the solid core can look up inside it, so the lower clamp runs for those alone (quadrature<true>): for every segment it cost 0.3 ms a frame. Bands of 32 rows are the tasks. The halving's bands take their rows, sums and source row from the caller's scratch, so a worker allocates nothing and a failed allocation throws where the caller catches it (QtConcurrent wraps a worker's exception in QUnhandledException). publish copies each tile and makes its selection coverage on the calling thread, so a failure throws there and a worker's malloc arena never meets a starved allocator, then fills side by side. Pieces compose and halve side by side (sideBySide), each halving unthreaded: Qt 6.4 deadlocks on a blockingMap inside a map task. The Lanczos colour rows sum four channels in a GCC vector (vector_size(16)), each lane as the scalar sum, and the vertical pass runs tap by tap over whole rows; BrushCoverage.cpp and DownsampleCache.cpp build at -O3, where GCC vectorises them. The shadow is skipped when the dirty rect lies inside the document, whose opaque fill covers it. Equivalent mutants: that skip, fill's skip of a zero weight, the colour row's right clamp (it only ever reads padding), the table's interpolation (under a third of a level), its lower clamp at step 1 (the first steps of every fade sit at the density's cap), the clamped quadrature for every segment, the formula for every hardness and tipDensity's two early returns (each only costs time). BrushPerformanceTests runs under BRUSH_BENCHMARK=1.
  • Canvas speed (11.1): CanvasPerformanceTests (under CANVAS_BENCHMARK=1, at a backing scale of two, as the brush benchmark) pans a 1000-point canvas by trackpad deltas through the canvas's own wheel path, at fit and at Actual Pixels (zoom(1), as the menu sets it), and zooms it in 5% steps around its middle, over a 3840 × 2160 document of ten noise layers (photographs and halves, opacities, multiply, screen, overlay and two Hue layers). Measured on the dev container (32 cores): pan at fit 30 ms median (p95 37), pan at Actual Pixels 68 ms (p95 74), zoom 77 ms (p95 86); with every layer normal 16, 15 and 29 ms. The hand-blended HSL modes are the cost: HslBlend::blend ran one thread over the layer's device area, 40 ms a Hue layer at fit; its rows now blend side by side (pointers taken first, since scanLine detaches), the same bytes, 3× faster (pan at fit 85 → 30 ms), and the mode is checked before the rows start, since a worker's exception comes back wrapped. At Actual Pixels a Hue layer still costs about 25 ms at 2× (the backdrop's copy and conversion, the enlarged surface and its draw back); the rest is QPainter's single-threaded smooth scaling, about 40 megapixels a frame at this size. Painting the canvas in parallel bands would need its caches (pieces, strokes, effects previews) made safe across threads, and is not done. The guards sit at half again these medians (45, 100 and 115 ms), so a regression fails. Compared with docs/brush-performance.md: Swift measures strokes only (2.6–2.8 ms median on macOS); the twin's stroke gate is 8.3's (6.8–7.4 ms at 4K, scale two), and Swift has no pan or zoom figures to compare. Rendering/PoolMap (Linux-only) wraps every QtConcurrent::blockingMap: Qt 6.4's deadlocks when a pool thread waits on a full pool (a worker's blur or blend on one core, or two workers on two), so a pool thread lends its place while it waits (releaseThread, then reserveThread, Qt's documented remedy); PoolMapTests runs the blend, the blur and a map on a pool of one thread.
  • Desktop integration (11.2): the app id is io.github.ZacharyZhang_NY.OmaPhoto (Compositor's was its bundle identifier, com.wonderassembly.compositor), which names the desktop entry, the icon and the MIME package, and which main gives Qt as the desktop file name, so a Wayland compositor matches the window to its entry and icon; on X11 Qt 6.4 names the window class after the application (omaphoto, OmaPhoto), so the entry's StartupWMClass is OmaPhoto. packaging/io.github.ZacharyZhang_NY.OmaPhoto.desktop launches omaphoto %F (files named at launch reach ProjectWorkspace::receive, as Finder's Open With reaches the delegate) and lists Swift's two document types: its projects (application/x-compositor-project) and JPEG, PNG, HEIC and TIFF. io.github.ZacharyZhang_NY.OmaPhoto.xml declares the project type for shared-mime-info, a folder (inode/directory) named *.comp, commented "OmaPhoto Project", where Swift says "Compositor Project"; a file manager may still open such a folder as a folder, since a project is a directory on Linux. cmake --install lays down the app, the entry, the MIME package, Compositor's app icon (packaging/icons) at six hicolor sizes (16 to 512) and U²-Net's licence under share/licenses/omaphoto; OMAPHOTO_MODEL, a path, installs the model as share/omaphoto/u2net.onnx whatever its own name, the file itself when given a link (file(REAL_PATH)), where SubjectRemoval looks, and packages pass it (11.3). A direct install (no DESTDIR) runs update-mime-database on its prefix, required, so the project type registers at once; a staged install leaves that to the package's hooks. DesktopIntegrationTests reads both files, stages an install and compares the whole file list (the model included when the build was configured with one, read from its cache), and installs directly with a stand-in update-mime-database first on the path and any DESTDIR of the caller cleared; the call in main is not under test.
  • Packages (11.3): one CMake install serves every package. It lays down the app, its entry, MIME package, icons and both licences (the app's MIT LICENSE and U²-Net's, under share/licenses/omaphoto), and OMAPHOTO_MODEL adds the model. OMAPHOTO_BUNDLE_ORT (off by default) installs ONNX Runtime beside the app in lib/omaphoto (under the library folder) with the app's run path set there, and Microsoft's LICENSE and ThirdPartyNotices.txt with it (OMAPHOTO_ORT_NOTICES, where the images keep them from the release; a configure that cannot find them fails). Every package bundles (11.4d, issue #5): ONNX Runtime stamps each release's symbols with its version (VERS_1.20.1), so a package linked against a system's runtime refuses to install, or to start, once that system updates it. The DEB, the RPM and the Arch package carry Microsoft's 1.21.0 build, pinned by checksum, which the dev, Fedora and Arch images install into /usr/local too, so the tests run against the runtime that ships; the flake builds from nixpkgs' own, since Nix rebuilds its closure with it. DesktopIntegrationTests expects the runtime (in the configured library folder), its version link and its notices in a bundling build, as it expects the model in a build given one, reading what CMake itself evaluated through compile definitions (both checked by hand in a second build). CPack makes the DEB (Ubuntu) and the RPM (Fedora), each on its own system: the DEB's library dependencies come from dpkg-shlibdeps, which finds the bundled runtime through the run path, and it names what no library does (libheif-plugin-libde265 for HEVC, qt6-image-formats-plugins for TIFF, shared-mime-info, hicolor-icon-theme); the RPM asks for qt6-qtimageformats, shared-mime-info and hicolor-icon-theme, its libraries found by rpmbuild but the bundled runtime left out of what it provides and requires (__provides_exclude_from, __requires_exclude in CPACK_RPM_SPEC_MORE_DEFINE), and suggests RPM Fusion's libheif-freeworld, since Fedora's own libheif decodes no HEVC. Packages stage their install (DESTDIR), so their systems' hooks and triggers refresh the MIME and icon caches. Dockerfile.dev gains dpkg-dev, file and fakeroot for the DEB. packaging/arch/PKGBUILD builds this checkout for Arch and Omarchy (makepkg there), fetching the model and Microsoft's runtime by their checksums; its prepare() lays the runtime out as that build's CMake config expects (lib64, include/onnxruntime beside lib/cmake) and the package bundles it. flake.nix sits at the repository root; it builds the app alone and installs staged (DESTDIR=/) so the store holds no MIME cache. The DEB was built and read back in the dev image; the RPM, the PKGBUILD and the flake are built on their own systems in 11.4.
  • Distro checks (11.4): scripts/distro-check.sh arch|fedora|nixos|resolute builds scripts/distro/<name>/Dockerfile (for resolute, Dockerfile.dev on Ubuntu 26.04; Fedora is 43 since 11.4d) and runs dev.sh's configure, build and full test run in it, warnings as errors, as uid 1000 (root ignores the read-only folders and unreadable files the tests make), the tree mounted read-only and HOME and TMPDIR on a tmpfs (a sync on the container's overlay waits on the host's disk); NixOS builds the flake's package (nixpkgs pinned by the committed flake.lock) as root, since Nix writes its store, then runs the tests in its devShells.default as uid 1000 (chroot --userspec, with HOME and TMPDIR put back, since nix develop points TMPDIR at a folder root made); the shell sets the plugin path, the model's data path and a fontconfig file of DejaVu with fontconfig's own rules from the store, since NixOS keeps them in /etc/fonts/conf.d, which a container lacks. Every image carries DejaVu Sans, Serif and Mono, the faces the tests name (Fedora splits them into three packages). Ubuntu 24.04 is the dev image itself. What newer Qt and GCC asked for: QImage::flipped from Qt 6.9 where 6.4 has mirrored; Q_OBJECT on every class a test finds with findChild (Qt 6.11 refuses one without); DistortWarp projects points itself (project in Distort.cpp), since QTransform::map clamps w for points past the horizon from Qt 6.10 (6.10.2 and 6.11.2 probed; 6.4 divides), where Swift divides whatever its sign, and DistortTests pins it past the horizon (on 6.4 the two agree, so a mutant there is equivalent); a local pragma for GCC 16's false -Wmismatched-new-delete in DownsampleTests' replaced operators. Tests follow the systems where only a message or a count differs: libheif 1.23's longer messages, Qt 6.9's TIFF plugin logging libtiff's errors as criticals, fonts named rather than counted, the Type icon's letters in the system's own sans (Fedora's is Noto: the icon sheet compares them only under DejaVu Sans), and QT_FORCE_STDERR_LOGGING for the launched app, since a journald-built Qt logs to the journal when stderr is no console. The five starvation tests place their failure by Qt 6.4's own allocations and skip on any other Qt (placesStarvation in AddressSpaceLimit.h); the limit itself holds there. ProjectExportFailureTests opens a panel and an alert once in initTestCase, before any limit, as the app's window has before any export: a snapshot that fails opens its alert while the limit holds, and on every system that alert was the process's first font lookup and the panel's first open, costs the app never meets there. In the NixOS shell, which reads fontconfig's whole conf.d without a cache, the lookup's failed allocation segfaulted inside fontconfig (FcHashTableFind, found under gdb); on the dev image a panel test run alone ran out inside the panel's first open. theJPEGRenderTakesTheSnapshotWithoutACopy holds the JPEG render's snapshot to a move under a 2 MB limit, as thePanelOpensWithoutCopyingTheSnapshot holds the PNG panel's. The flake leaves out dev.sh's build/, which nixpkgs' CMake hook would take for its own build folder.
  • Spot Healing (8.4): HealPixels.c compiles in place beside the other kernels. SpotHealingMode, allSpotHealingModes and rawValue (Swift's names) sit in BrushStroke.h, and BrushSettings gains healing and healingMode. While the stroke lasts its tiles show Swift's wash, gray 0.12 at 45% through the coverage, never erasing. heal takes the painted bounds from the kernel's heal_coverage_bounds, draws the source into a region about three spots wider, and hands the kernel the coverage packed row by row, as Swift's gray context holds it (the twin copies each tile's rows where Swift draws each coverage image); the seed is QRandomGenerator::global(), Swift's UInt32.random. Each touched tile is then its base with the healed region copied over, blended with the base through PixelAdjust::blend under a selection, where Swift clips the context. The kernel's failure is ProjectError(tooLarge); pixels or packed coverage that cannot be allocated are ExportError(render), as Swift's contexts. The session's beginBrush takes Spot Healing on pixels alone, never on a mask, as Swift's guard; finishBrushImmediately heals before the commit, one Spot Healing step; spotHealingMode is a getter and a setter that notifies; Tab steps the type. isBrushTool is Swift's four tools: the circle, the right-drag tip, the brackets, the digits and BrushControls serve each, and a press with Clone Stamp or the Smear begins nothing until 8.5 and 8.6 bring them. ContentView keeps one bar across the four, as Swift's single isBrushTool branch keeps its view; the bar titles itself by tool (Spot Healing, Clone Stamp, Smear, Eraser, Brush), shows Paint and Erase for the Brush alone and the three types for Spot Healing alone, and names opacity Strength for the Smear; the clone's Aligned and Sample arrive with 8.5, the Smear's modes with 8.6. Equivalent mutant: the healed tile starting from its context rather than its base, since the region holds the whole wash and the copy replaces it.
  • Clone Stamp (8.5): Document/CloneStamp.h holds CloneSettings, CloneStamp.cpp Swift's extension: setCloneSource (a point that is no number is refused; a new source restarts the alignment), cloneStrokeOffset, cloneSamplePoint and cloneSample, whose context that cannot be allocated logs on omaphoto.app where Swift's try? is silent. The session keeps cloneSource, cloneSettings (a setter that notifies), cloneOffset and parkedBrushTips by tipFamily (Clone Stamp 1, the Smear 2, the rest 0; the two parked tips start at Swift's 40, 0, 1). selectTool parks size, hardness and opacity when the family changes and reads the tip it restores with at: the session starts in family 0 and leaving a family parks it, so Swift's if let always finds one. beginTransform and Transform Selection set the Move tool directly, as Swift's do, and park nothing. beginBrush takes Clone Stamp on pixels alone, asks for a source first (brushError: "Alt-click where Clone Stamp should copy from first."), samples the document as the stroke starts and fixes the offset; Tab toggles the sample; the step is Clone Stamp. BrushStroke::Clone is Swift's tuple. publish makes each tile's view of the sample on the calling thread (clonePixels): under a scale or turn QPainter draws it bilinear, Swift's medium quality; a whole translation copies it; a fractional one is drawn at the floor and shifted by BrushRaster::shiftedByFraction, since QPainter snaps an unscaled image. fillClone then composites it source-over through coverage × opacity × selection, each byte rounded once, where Swift clips the context to the coverage. Swift's !isMask || isBlur holds since 8.6: a mask takes a clone only from Blur. The bar shows Aligned (a QCheckBox written on clicked, so the session's own changes write nothing back), This Layer and All Layers, and "Alt-click to set the source" in PlaceholderText while there is no source. On the canvas an Alt-press with Clone Stamp sets the source and paints nothing; Swift's cursor refresh there is dead (Alt is held, so the crosshair stays) and left out. With a source the pointer hides unless Alt or Space is held. updateBrushCursor gives the overlay the source crosshair in view points and, between strokes and without Alt, clonePreview (the source around the stamped point at screen resolution, at most 1024 pixels, kept under Swift's key; Swift's .medium there is overridden by LayerRenderer's own quality and left out) with cloneTip (one click painted by the brush engine, kept by size and hardness). BrushCursorOverlay gains marker, preview, previewOpacity and tip, images compared by cacheKey as Swift's !==; under an antialiased ellipse clip at the brush's opacity it draws the preview and cuts it to the tip inside AdjustmentSurface::draw, Swift's transparency layer, so the tip applies at the drawing's resolution however far a capped preview is enlarged, and draws the crosshair white 3 under black 1 with round caps. A modifier change refreshes the brush cursor, as Swift's monitor does. Equivalent mutants: displayedTransform in cloneSample and clonePreview (it differs from the stored transform only in a transform edit, which leaves Clone Stamp for Move, or a filter edit, which refuses strokes; the preview would show it only with a filter sheet open over Clone Stamp).
  • The Smear (8.6): SmudgeLiquify.h/.cpp hold BlurToolMode and WarpStroke, Swift's float arithmetic to the letter (the positive guards of weight included) on a document-sized copy drawn through the layer's displayed transform; image() is that copy itself, where Swift snapshots it after each append, since the twin reads it only between appends. beginWarp and finishWarp twin the extension: a warp takes pixels alone (brushError: "Smudge and Liquify work on a layer's pixels, not its mask."), and the commit paints the copy along the stroke with a hard tip four pixels wider, replacesWithClone so clear pixels clear, one step named by the mode. BlurTool.cpp holds blurSample: sigma a tenth of the brush within 1.5 and 30; the layer blurred with clear past the document's edge; a mask on its edge tone, its placed box black under its coverage, blurred with the edge repeated. Core Image's Gaussian is PixelAdjust::gaussianBlur: separable, weights exp(−i²/2σ²) over three sigmas, normalised, in float on the stored premultiplied values (Swift's Core Image context is unmanaged), each byte rounded once, rows in parallel. Where Core Image approximates a large blur, the twin's is exact; it costs 20–110 ms for a 4K document on 32 cores (sigma 1.5 to 30), paid as a Blur stroke begins, since Swift's sample is taken whole. BrushStroke gains isBlur (a mask takes a clone only from Blur), replacesWithClone (a copy by coverage: fillClone keeps 1 − coverage of the old pixel) and editName (the step's name when set). clonePixels resamples every clone, a gray one too, into premultiplied colour, so a sample's edge keeps the share of the pixel the image covers, and fillClone composites that alpha into a mask as CoreGraphics draws a gray image's edge: a white mask half a pixel past the canvas stays white. The session keeps blurMode and warpStroke; beginBrush hands Liquify and Smudge to beginWarp; continueBrush, cancelBrush and finishBrushImmediately take the warp first; exactly Swift's six places read it: canEditLayers, canUseHistory, canStartProjectOperation, selectLayer, selectTool, cycleToolMode (selectLayers and selectLayerTarget read the brush stroke alone, as Swift's). Tab steps the Smear's modes. The bar shows the three modes with Swift's help; the status line says "Drag to soften", "Drag to smudge" or "Drag to push pixels" (hint takes the Smear's mode). On the canvas the warp's copy stands in for its layer across the canvas, through the layer's mask at 2048, its opacity and blend mode, and a step repaints the view (no dirty rect, as Swift's); drags, releases, Escape, wheels, pinches and the tip drag treat a warp as a stroke. Equivalent mutants: displayedTransform in beginWarp and blurSample (painting refuses transform and filter edits), the radius rounded to nearest (pixels past its floor lie outside the brush), u >= h for u > h (weight 1 either way), a warp without dabs going on to commit (it appends nothing), copy mode weighing by coverage times opacity (a warp commits at full opacity), fillClone's format guard (a clone is always sampled in colour), and the Gaussian's branch-free interior moved by a pixel (it reads one tap weighing under 1% of the centre).
  • The Gradient (8.7): Document/Gradient.cpp twins Swift's extension. GradientEdit is a struct in a std::optional, where Swift's is a class; its raster is a std::shared_ptr<BrushStroke>, as a pixel move's, because the canvas draws it while the commit renders, and the raster's pointer stands for Swift's gradientEdit === edit (a new line on the same target keeps the raster, as Swift keeps the edit). commitGradient takes a completion callback, posted on every path; resolveGradient posts the commit, Swift's Task, so a switch of tool, layer or target applies it once the caller returns; invertPixels and beginFilter commit it first and call themselves again from its callback, as Swift awaits it. fillGradient fills the canvas through paintCanvas with a QLinearGradient, or a QRadialGradient centred on the start with the line as its radius; Qt's pad spread is CoreGraphics' drawsBeforeStartLocation and drawsAfterEndLocation, and two stops interpolate alike in both. gradientColors makes a mask's colours gray from the palette's red, where Swift makes DeviceGray colours. undo first discards a pending gradient and canUndo counts one, as Swift's. The palette's model came forward from 8.10 into ColorPalette.cpp (PaletteColor, foregroundColor with setForegroundColor through setBrushSettings, backgroundColor, canEditPalette, paletteColor, setPaletteColor, swapPaletteColors, resetPaletteColors), and fillSelection reads it; the Type bar's share of setPaletteColor came with 8.9a, the picker and the swatches came with 8.10a. On the canvas (EditorCanvas+Gradient.cpp) a press within ten points of the end, then the start, grabs it, else begins a line; Shift turns the dragged end to a multiple of 45° around the other; Escape cancels and Return or Enter applies, after the selection's keys; losing focus drops the drag. TransformOverlay draws the line in place of the handles while one is pending, as Swift's: a radial's dashed rim at the line's length, the line black 3 under white 1, each end a white disc with a black rim, a gray ring and its colour. A pending gradient on a folder's mask clips the folder's layers at once through liveFolderMaskClip, as a brush stroke does. GradientControls is the tool's ToolHeaderBar: the shapes as checkable QToolButtons in a QButtonGroup, GradientSwatch (white, gray checks of four points, the colours, clipped to a rounded rect, the rim unclipped as Swift's strokeBorder overlay), the style QComboBox, Reverse, the slider in thousandths from 1% and the percent field (a SelectionAmountField, whose own clamp stands for the binding's), the Mask note in PlaceholderText, and Cancel and Apply while pending. Equivalent mutants: the gray branch of gradientColors and of fillSelection (a mask's palette is black or white), and the swatch's rim clipped too (3/255 at its corner pixels, measured). A press that marks a drag without an edit was equivalent until 8.10b's Alt eyedropper read the drag; a test holds it now.
  • The Shape tool (8.8): Document/ShapeTool.h/.cpp twin Swift's file, ShapeTool+Session.h its session half. ShapeKind's raw values, shapeKind(text) for the manifest and path stand for the enum and its path(in:cornerRadius:); LayerShapeStyle, LayerShape and ShapeDraft keep Swift's fields, ShapePreview is Swift's tuple. A LayerShape holds its image's ImageIdentity, Swift's ===, since ImportedImage::image() is never cached in a model type; ImageLayer.shape joins equality, and liveShape() compares identities, so any other change to the pixels leaves a shape that is no longer live. addPixelLayer takes the style and ties it to the asset it makes, where Swift passes a LayerShape made from the same image. shapeKind, shapeCornerRadius and shapeLineWidth are getters and setters that notify, shapeDraft a getter. finishShape truncates each side before multiplying, as Swift's Int(), in doubles; a Line click leaves a dot as wide as the line, since the box grows by the thickness, as in Swift. strokedLine gives a line's round-capped outline, filled on the layer and on the draft, because QPainter draws a pen a pixel wide through its thin-line path, whose diagonals cover less than CoreGraphics' stroke; a zero-length line is a disc, since CoreGraphics caps it as a dot (PDF's rule for round caps) where Qt draws nothing. redrawShape pins any mask where it shows (Swift's placement == nil test changes nothing: a placed mask's maskTransform is its placement) and logs on omaphoto.app where Swift's try? is silent; shapeTransformPreview is const with a mutable cache, reads the pixel size from the asset a live shape shares, and logs on omaphoto.rendering. Left out as dead or redundant: path's max(0, …) (its radius > 0 test sends a negative radius to the plain rectangle), shapeLineEnds's kind test and the draft drawing's guard let ends (only a line's drag sets an end, and its box with it), finishShape's zero-width fallback in unit (the box is a pixel wide by then), redrawShape's max(1, …) (a valid transform is a pixel wide), and the canvas drag's lastDragPoint == nil (a pan never begins a draft). Hooks: selectTool cancels a draft, Tab steps the kind, commitTransform redraws each moved shape, the record carries a live shape's style (hierarchyRecord), installProject loads shapes and applyDocumentSize does not, as Swift's rebuild leaves them out after a crop or a resize; where Swift rebuilds a layer (a brush or raster commit, Invert, a filter, a floating merge) the twin drops the shape in place; the canvas resizer passes it on, the image resizer not; a duplicate or a copied layer keeps it. The manifest's shape key is Swift's Codable: the kind's raw value, the numbers, lineWidth, start and end only when set, points as two numbers; a wrong type or a missing key refuses the project. On the canvas (EditorCanvas+Shape.cpp) a press begins, a move drags (Shift squares or snaps a line, Alt grows from the centre), the release finishes, Escape cancels after the selection's keys, Shift-U steps the kind with the Shape tool and chooses it otherwise, losing focus cancels. The draft draws above the active layer, inside the live mask renderer's drawOwn, composed aside through LayerRenderer::composite so folder masks clip it; a line at its width times the zoom, a point at least, with round caps. Swift's draw reads the draft under AppKit's observation tracking; the twin's synchronizeDisplay compares ShapeShown (draft, colour, width, the layer it sits above) and redraws the view as that observation does, where the brush repaints its stroke's rect alone; DisplayState counts a blank layer while a draft sits on it, so moving or hiding the target redraws too. A rounded rectangle resized with the Move tool draws shapeTransformPreview in place of its pixels. ShapeControls is the tool's ToolHeaderBar: the kinds as checkable QToolButtons in a QButtonGroup (a choice drops the draft first), Width for lines (a slider of 1–100 showing at most 100, a SelectionAmountField of 1–5000, "px") and Radius for rectangles (0–200 and 0–5000, with Swift's help); the sliders show the session's number without writing it back; the Fill swatch came with 8.10a. The status hint follows the kind. Known difference: a Line layer's box follows the dragged point, so it can sit between pixels, and at 1:1 and in exports QPainter draws it on whole pixels, where CoreGraphics interpolates. Equivalent mutants: Qt's addRoundedRect adds a plain rectangle at radius 0, so radius > 0 and >= 0 agree; an undragged draft's box is its anchor, so finishShape's value_or may take either; the draft drawing's empty-box test for a rectangle or ellipse only spares an empty surface, since a path of no area paints nothing.
  • The Type tool (8.9a): Document/TypeTool.h/.cpp twin Swift's file, TypeTool+Session.h its session half. TextAlignment's raw values and textAlignment(text) serve the manifest; LayerTextStyle, LayerText and TextDraft keep Swift's fields. A LayerText holds its image's ImageIdentity, as a LayerShape does; ImageLayer.text joins equality and liveText() compares identities. TextKit has no Linux twin, so Rendering/TextLayout is Linux-only: TextLines lays a style out with QTextLayout as textBoxSize and textImage lay it out with TextKit. Lines have a fixed lineHeight with the baseline at their bottom, so extra leading sits above the letters (Swift's equal minimum and maximum line heights); they wrap at word boundaries, a word longer than the line anywhere (byWordWrapping); a line that would pass the container's bottom is not laid, nor anything after it (overflows); a final newline adds no line to a measure (its line holds the caret alone, 8.9b), and an empty paragraph lays one, the caret's room. Paragraphs part at Cocoa's separators, LF, CR, CRLF and U+2029, and Qt breaks a line at U+2028 by itself. Tabs stop every 28 points, as NSParagraphStyle's twelve default stops do up to 336. Past the twelfth Cocoa has no stop and moves a tab down a line (a line of tabs wraps after 336 points in an NSTextView); here the stops go on every 28 points (known difference): moving such a tab down would lay the rest of its paragraph again for each one, quadratic in a run of tabs, and measure bidi lines by hand, while QTextLine::setNumColumns leaves the tab hanging on the line above. Faces go by PostScript name, as macOS names them: fontconfig lists each installed face's FC_POSTSCRIPT_NAME with its family and style (availableFonts, the bar's list). A name it does not list is Qt's match for that family, where Swift falls back to the system font; fontconfig's aliases put Helvetica, the default, on a metric twin where one is installed. The size is the style's in pixels, as a CoreGraphics context counts points, rounded: Qt draws a face in whole pixels (QFontPrivate::engineForScript rounds a point size's pixels too), so a fractional size draws at the nearest whole pixel where CoreText draws it exactly (known difference; the default is whole, and the bar's steps keep a whole size whole). Kerning is off, since Swift always sets .kern; tracking is absolute letter spacing after every letter, the last included, as CoreText kerns; hinting is off, so Qt lays out design advances by itself (QFontEngineFT::shouldUseDesignMetrics) and the layout sets no flag for them. Qt 6.4 scales absolute letter spacing by the device's dpi over its own in whole numbers, so on a 72-dpi image it vanishes (probed): lines are laid out without a device, where that scale is one. Letters differ from macOS's where the faces differ; the rules are the same. Dockerfile.dev installs libfontconfig-dev; CMake links Fontconfig::Fontconfig. In the session textDraft has a public setter, as Swift writes it freely, and textDefaults a getter. applyText takes its draft by value, as Swift's struct is passed: finishText hands it the session's own draft, which it clears. It clears the draft first (the gates read it) and puts it back on refusal, silently, and before an error is announced, so the error's signal sees the draft; blank new text and an unchanged edit close the draft, announced, with no step and no canvas focus request. layerName keeps 40 graphemes (QTextBoundaryFinder), Swift's prefix(40) of Characters; simplified() makes whitespace runs one space. recolorText logs a failed redraw where Swift's try? is silent. Left out as dead: the isFinite terms of isValid and boxIsValid (the ranges refuse NaN and infinities); the draft and document guards of beginText, editActiveText and recolorText (canEditLayers reads both); textBoxSize's max(16, …) on both sides (the padding alone makes 24) and its caret line (an empty paragraph lays a line); textImage's width >= 1 terms; recolorText's validity guard (textImage refuses an invalid style, and the refusal is logged); setPaletteColor's Type-tool test (a draft lives only under the Type tool: every way in sets the tool, and a switch finishes text first); fillSelection's liveText test (recolorText asks it); the store rule's folder term (text needs pixels, and LayerHierarchy::validate refuses a folder that has them). Hooks: the gates and canStartProjectOperation read textDraft; selectLayer, selectLayers and selectTool finish text first when the choice changes, and stop when that is refused; the foreground swatch colours an open draft; Fill recolours live text when neither a selection nor a mask is in play; addPixelLayer takes the style, as it takes a shape's; installProject loads text and applyDocumentSize does not; the record carries live text; every rebuild that drops a shape drops text; the canvas resizer passes it on, the image resizer not. The manifest's text is Swift's Codable: every key required but boxSize (synthesized decoding ignores the defaults), the alignment by raw value, the box as two numbers; the store refuses a project whose text fails isValid or has no pixels (Swift's validate; its adjustment term is dead, since an adjustment layer's own rule refuses pixels, 9.5a), and a snapshot installed past the store loads such text as pixels (LayerText::loaded). TypeControls is the tool's ToolHeaderBar: the face as a TypeFontPicker (since 12.1: its list of fontconfig's names and the style's own, sorted, loads when the menu first opens); size, tracking and leading as TextStyleFields, QLineEdits that take each readable edit as Swift's value bindings do, show the session's number unless focused, step by one on the arrows (Shift ten), keep their typing while a menu or popup borrows the focus, and keep the focus on Return, showing the number again, selected, as an NSTextField does: Swift's Type bar alone has no releasesFocusOnCommit; Escape is the field's own. They print Swift's .number: at most three decimals, none trailing; digits are not grouped, as in every field of the twin. The size's parse check is left out: text that is no number reads as zero, which no size passes. The size steps within 1–2000; the leading shows Auto as its placeholder, clamps what is typed to 0–5000 and steps from the height Auto works out to. The alignments are checkable QToolButtons with Swift's text.align glyphs drawn as paths, redrawn on PaletteChange as the Move bar's lock is; Cancel and Done show while a draft is open, else Edit Text, enabled on live text; the bar rests without a document or while busy shows. The fields scroll as the Move tool's do. The colour swatch and openTextColorPicker came with 8.10a. A live text row in the panel shows the rail's Type glyph 1.2 times up, centred (Swift's textformat, 1.21 times up), "Editable text layer", "Select text: " and "Text" (Swift 1.1.5's words); ThumbnailKey carries editableText, so text that loads as pixels redraws. A double click on a control makes the row active, then opens its text (Swift's renameClickedLayer); on the name it renames. The inline editor, the canvas's gestures and the edited layer hidden under its draft come with 8.9b. Equivalent mutants: ImageLayer's equality without its text term (every text change brings new pixels); TextLines without its stop after a paragraph that overflowed (every later paragraph overflows at once, so the stop spares only their layouts).
  • The inline editor (8.9b): Rendering/InlineTextEditor.h/.cpp twin Swift's file; InlineTextEditor+Editing.cpp holds its text view's keys, moves and undo, InlineTextEditor+Input.cpp its input methods and clipboard. Swift's editor hosts an NSTextView that AppKit turns and mirrors with the layer; a Qt child widget cannot turn, so the canvas paints the editor itself, as it paints TransformOverlay, and hands it its keys and input methods, and what NSTextView gives for free (selection, typing, deletions, movement, undo, the clipboard, marked text) is written here on TextLines, the layout textImage draws. synchronize follows Swift's: the logical size is textBoxSize, point text on a layer grows by the width's factor with its corner kept, handles are six view points (two at least), a style changed elsewhere clamps the selection unless text is marked, and a fresh draft clears the undo steps and takes the keys, posted, unless a text field (QLineEdit) holds them. The canvas synchronizes on every changed(), so a draft that ends takes its editor with it; Swift's fresh branch is reached here only when the session's setTextDraft sets one draft over another, as SwiftUI's coalesced updates reach it, and a test does that. The box maps layer pixels through the layer's unit square without flips; the text surface mirrors about its middle. The frame (accent, a sixth of a handle wide, inset a twelfth), the eight white handles with accent rims and the black overflow plus draw first and the text over them, as a subview draws over its view (clear glyphs since 12.12a); the caret is a unit wide and a line tall in the text's colour, shown with an empty selection while the canvas has focus, blinking at half the system's flash time; a selection is Highlight, the inactive one without focus, drawn per line from Qt's own visual runs (a FormatRange background, the line's own height stretched to the fixed one), so mixed directions highlight what is selected, and where it passes a line's start or end it runs on to that line's edge on the side the paragraph reads, as TextKit's block selection; marked text is underlined unless the input method styles it. TextLines gains TextRange, lineOf, caret, lineStart, lineEnd (a line that ends short of its paragraph ends before its breaking space, laid next line or not: the caret has no affinity, where TextKit's upstream caret sits after it), position and a draw with a selection and marked text; it flags overflow only when a character is left unlaid, TextKit's glyph test, so an empty box shows no plus. After the text's final break (LF, CR, U+2029 or U+2028) it lays TextKit's extra line for the caret, even past the container's bottom (point text's measure, boundingRect's, leaves it out), so Home, Up and typing there stay on it; measures and overflow ignore it, and the editor's repaint reaches a line below the box. A U+2028 that ends any paragraph gets the empty line Qt's own createLine makes after it, as TextKit breaks there, and a line ends before the space or separator that breaks it. A position past the laid text has no line (lineOf is empty past m_laidEnd, where Qt's lineForTextPosition would answer the last laid line for a paragraph's end): there the caret hides, Home and End go to the text's start and end, and Up and Down stay put. Keys: Swift's own (Escape cancels, Alt with the arrows sets spacing, Ctrl+Return finishes), the fills' text meanings (Alt+Backspace and Alt+Delete a word, Ctrl+Backspace and Ctrl+Delete to the line's start or end; Shift adds nothing, as NSTextView's Shift-Delete deletes back, and the keypad's Delete deletes), then Qt's standard keys for undo, redo, the clipboard, select all and movement, as the app's text fields take them; X11 binds Alt+Backspace to Undo and Shift+Delete to Cut, so deletions come first and Shift+Delete stays Cut. Return types a newline, Tab a tab, and typing is what QInputControl::isAcceptableInput accepts, term for term (a format character before the Ctrl test, AltGr's Ctrl with Alt typing). Characters are graphemes and words QTextBoundaryFinder's; in a paragraph that reads right to left (its first strong letter, as Qt judges), Right goes back and Left on, words alike, and a plain arrow collapses a selection to its own side, as Qt's fields move in their logical style; Up and Down keep their first x and reach the start or end past the lines. Undo is the editor's own stack: typing and deleting each coalesce until the caret moves, paste and cut are steps of their own, and a fresh draft starts empty (Swift's removeAllActions). Marked text lives in the draft's content, as NSTextView keeps it in its storage, but history follows Qt's input method contract: the preedit records nothing, and what commits or removes text makes a typing step from the text before the preedit (joining a typing run, else its own), so a cancelled composition keeps the redo and undo while composing takes the last step back. Each event follows QWidgetLineControl::processInputMethodEvent on the text without the preedit: input (a commit, a changed preedit, a replacement) takes the selection first, then the replacement counts from the cursor, a range removed whole or not at all, a zero length inserting at its offset, and where nothing commits the cursor goes where Qt leaves it; an event that is no input changes nothing (Qt's fields remove a selection only on input, and ibus sends empty preedits when an engine switches modes). A Selection attribute sets the selection after the commit; while a preedit shows, that selection stays beside it (the preedit's anchor, in the text without it), drawn and answered to queries, until the next input takes it, and the attribute ends a typing run as a move does; a draft set over a composing one ends the composing; TextFormat attributes style the preedit, counted from its start (a format that is none is passed over, as Qt's fields do), else it is underlined; a Cursor attribute places the caret, hides it at length zero and colours it with a QColor value. Copy, Cut, Paste and Select All, from the menu or their keys, work around a preedit as Qt's fields do: on the text without it and the selection beside it (plain), the preedit then going back at the cursor (settle), so Select All then a commit replaces everything; undo takes a preedit away with the step, since it records nothing. Other keys reach the editor while a preedit shows only when the input method lets them through, and act on the text as it stands: typing and deletions end the composing, the preedit staying as text, as NSTextView unmarks it. Text holds 100,000 UTF-16 units (shouldChangeTextIn). The canvas makes the editor in synchronizeDisplay (Swift's synchronizeInlineText, whose twin lives in InlineTextEditor.cpp), draws the edited layer as its typed pixels since 12.12a; a draft that begins, ends or is replaced repaints the whole canvas, as Swift's needsDisplay, because the editor's box need not cover the layer's (text shrunk while edited, a layer stretched one way, which point text shows at its width's scale); while a draft is open every key goes to the editor (a key it leaves goes on to QWidget::keyPressEvent, which ignores it for the ancestors, none of which takes keys: its mutant is equivalent; so is the blend keys' focus term, since canEditLayers refuses a blend step while text is open), the text view's standard keys and Backspace with any modifier (the fills' keys; Delete has no menu key and needs no claim) win ShortcutOverride ahead of the menus, Shift+Tab stays put, focus shows and hides the caret (its blink is caretBlink, a named child timer of the canvas, running only with focus, so tests read it), input methods are enabled through setAttribute alone (Qt updates the input method itself and commits a preedit on the way out, so the editor goes first and that commit finds none) and follow the caret: every edit, move and view change updates them while the canvas is the focus object, as QWidget::updateMicroFocus, and a focus request leaves the keys where they are, as Swift's consumeFocusRequest. The menus follow Swift's text branch: with a draft open Undo and Redo are bare and enabled, and Undo, Redo, Cut, Copy, Paste and Select All reach the editor of the focused canvas, a text field's first. The pointer (placing the caret, dragging a selection, the handles, the text box, the cursors) comes with 8.9c.
  • The pointer on open text (8.9c): InlineTextEditor+Pointer.cpp holds the pointer half of Swift's file: the canvas's text gestures (the CanvasView extension there: beginTextGesture, dragTextGesture, finishTextGesture, drawTextBoxDraft) and what the editor view and its NSTextView do with the mouse. A Type-tool press goes to the open editor first (press, Swift's hitTest): a handle's band (Swift's handle(at:): ten points to a six-point handle, a third of the box at most) resizes; a press inside the box but in the text's padding does nothing, as Swift's editor view; inside the text it is the text view's click; elsewhere beginTextGesture finishes the open text, then opens the topmost visible live text under the point, its click forwarded to the text view wherever it lands (pressText, as Swift forwards mouseDown), or starts a box. A release under four pixels both ways is point text, else beginText(in:); Escape (ahead of the editor's keys) and another tool drop the box, drawn a point wide in the accent, last; QPainter strokes a one-point pen without joins (its cosmetic stroker, probed), so the box's four outer corner pixels stay clear where CoreGraphics' miter covers a quarter (known difference). Swift's needsDisplay at the end of beginTextGesture is left out as dead: opened text repaints the canvas, and a box of no size draws nothing until its drag repaints it. A box whose document goes mid-drag (an undone New Canvas) draws and drags nothing and makes nothing on release, Swift's guards in dragTextGesture and drawTextBoxDraft (the twin's in textBoxDraftRect, which both read). Clicks follow NSTextView: one places the caret at the nearest position, two select the word under the pointer (QTextBoundaryFinder words; the character under it by CursorOnCharacter, TextLines::character), three the paragraph with its break, and a drag extends by the click's unit from the unit it began on, only while the left button is held. Above the lines is the text's start and below them the end of the laid text, where NSTextView puts a click or drag past its lines. An empty last line has no word or paragraph (the unit is empty there, where a lookup at the last character took the line before); past the text's end a double click takes its last word. Shift extends by the last click's unit (NSTextView's kept granularity, QWidgetTextControl's double-clicked word) while the selection that click made stands: its press drags on from that unit, and a selection set otherwise brings the caret's own unit back, extending from the anchor: keys, and any change of style or text, since Swift's style sync sets the selection anew (setSelectedRange, which resets the granularity); a fresh draft of the same style keeps it, as Swift's. The row under the pointer is judged as a double before it becomes an int: a far drag over a squashed layer passes int's range. Qt has no triple click: the press right after a double click counts three when it comes within the double-click interval and the drag distance, as QWidgetTextControl times it; a press elsewhere ends the wait, as AppKit counts each click from the one before. Clicks map through the text's own mirror, so the caret lands where the letters show; Swift's NSTextView takes them unmirrored, because AppKit's hit testing ignores the layer transform that mirrors it. A click while a preedit shows asks the input method to commit, Qt's rule for a moved cursor (QInputMethod::commit, a no-op offscreen, so the call and the guard before it are equivalent mutants there), and lands the preedit as typed if none does, as NSTextView unmarks. A handle's press makes point text a box of the size it shows (Swift's mouseDown, whose fixed.origin is left out as dead: new point text shows at its own origin, and text on a layer commits its transform); its drag sets the box in its own turned frame, 16 source pixels a side at least, rounded, the far edges kept, and refuses a box or transform that is not valid. The cursor over a band is the frame's resize cursor turned with the box by eighths (Swift's handleCursor on Qt's four resize shapes; the rotation is reduced by 360 before the eighths are counted, since Swift's Int holds turn counts an int cannot), the I-beam elsewhere; a resize keeps its cursor, and its release shows the one under the pointer, as every release on the canvas does. AppKit always sends mouseUp to the view that took mouseDown; Qt can lose a release (a popup or modal dialog opened mid-drag, a grab the compositor takes), so a move without the left button, a lost focus or the next press ends a text gesture: a box being drawn goes, repainted away, and a selection or resize stops where it is. An edit (typing, an input method, undo, the spacing keys) ends a drag or a resize as a press would: Swift's resize would write back the draft it began with, dropping the edit, and AppKit's tracking loop holds a selection drag's keys until the release (known differences, the first a fix). Equivalent mutants: drawTextBoxDraft's save, restore and brush (it draws last, on a painter each overlay before it restores) and its guard (no box maps to a null rect, which draws nothing). Known differences: NSTextView's context menu and its dragging of selected text are AppKit's own; the Edit menu carries Cut, Copy and Paste. arrowPath and duplicateCursor moved to EditorCanvas+Cursors.cpp, keeping the navigation file under 500 lines.
  • The palette and its picker (8.10a): Document/ColorPalette.h/.cpp twin Swift's file. PaletteColor gains quantized, hex and fromHex (Swift's init?(hex:)): spaces and tabs trimmed as Swift's .whitespaces trims them (Unicode space separators and the tab, never a line break), one leading #, three digits doubled, six required, then Swift's UInt32(_:radix:) read by hand: an optional sign, a minus holding zero alone, no 0x (Qt's toUInt takes 0x and white space). PickerHSB keeps Swift's maths to the letter (fmod for truncatingRemainder, Int(h) truncating) and its setRGB: grays keep the hue, black the saturation. ColorPickerTarget is a struct with a Kind, as TransformDrag::Mode twins an enum with payloads: the palette and text, the Gradient Map's ends since 9.4a, an effect's colour with 9.6. ColorPickerState is a plain struct in the session with an id for Swift's object identity (a new picker opens its panel anew); Swift's unused background and PaletteColor(NSColor) are left out. The sheet writes the working colour through setColorPickerHSB, which notifies, where Swift writes colorPicker.hsb in place; closeColorPicker clears the picker before applying it, so each signal sees it closed. sampleCompositeColor draws the live composite into one premultiplied pixel at the floored point and undoes the premultiplication as Swift does; a composite that throws logs on omaphoto.app and samples nothing. UI/FloatingPanel is Swift's FloatingPanelController, brought forward from 9.1: a QDialog with Qt::Tool, non-modal as a dialog is unless asked, fixed to its content's size, remembering its top-left by name for the run: every show and every move while shown remember it, so a panel dropped without closing keeps its place and a close has nothing to add (Swift's close and windowWillClose remember again); first shown over the canvas's middle (the owner window's CanvasView), else centred on the window, as a QDialog centres itself on its parent; a shown panel keeps its place, since Qt keeps a window's top-left as its size changes where AppKit keeps the bottom-left (Swift's re-pin has no twin); every show raises it and hands it the keys, Swift's makeKeyAndOrderFront; its close button and Escape report through onClose, as windowWillClose does, and close hides without reporting (Qt's hide sends no close, so Swift's dismissing has no twin); refocus activates the shown panel of a name; new content is shown at once, so the panel takes its size at once (a layout shows late children from the event loop), and old content is hidden at once and deleted later, since a signal from it may still run (the layout lets it go then). QDialog::closeEvent would reject, and so close, again: the panel's close accepts directly. Known differences: a Wayland compositor places windows itself, so the remembered places hold on X11; hidesOnDeactivate has no Linux twin. UI/ColorPickerSheet twins Swift's file: the 256-point field (white to the hue across, clear to black down, a 60% black frame, the marker's black ring behind its white one), the hue strip (360 on top, arrows in the text colour), the preview (a 60% black rim), OK as the default button and Cancel, the R, G, B and hex entries and "Click the canvas to sample" (sampling arrives with 8.10b). The field and the strip pick with the left button alone, as SwiftUI's DragGesture. The strip's arrows reach five points past its ends, as SwiftUI lets them overflow; a widget clips its painting, so the sheet draws them over the strip's place and repaints that room. Every control carries Swift's accessibility label as its accessible name (Saturation and brightness, Hue, New color, Red, Green, Blue, Hex color) and every entry Swift's title as its placeholder; Qt's plain widgets hold no settable accessible value, so the hue's rounded degrees are the strip's description. The entries are PickerFields, committing on Return and on leaving, where a menu or popup borrowing the focus keeps the typing; an entry keeps its typing only while it holds the keys and was typed in, and a pick on the field or the strip, which take no focus, replaces the channels' typing (9.6f): a channel takes digits alone, clamped as Swift's binding (an overflow reads as 255, an empty or untouched entry changes nothing), Up and Down stepping one, or ten with Shift, from the colour's own value; the hex commits on every leave, as Swift's onChange(of: hexFocused), which snaps the working colour to its 8 bits. Return in an entry commits it and goes on to OK, since a QLineEdit passes Return to its dialog; Escape cancels. The channels are made before the hex entry, so the focus chain reads the grid and the keys start at R, which follows the colour until typed in; the hex entry follows only while it lacks the keys, and holding them first it would put its old text back on leaving. ColorPickerPanelController shows a new sheet per picker, titled by its target; its close button cancels, and Swift's guard there is left out, since closing no picker does nothing. UI/ColorPaletteControls twins Swift's file under the rail: two 24-point swatches (a 1.5 white ring inside a black rim), the swap (X) and defaults (D) glyphs drawn as paths in the secondary ink, dimmed while disabled, the whole resting while the palette cannot be edited, both swatches repainted whole on every change (they overlap); a mask's swatch offers Black · Hide or White · Reveal in a popup under the swatches (Swift's popover), which a change of target closes, and a mask chosen cancels an open picker; the panel follows the session's picker by its id. Swift's frame is 36 points and its two buttons reach 3 points past it, so the twin's widget is 42 points square with the frame inset by 3, in a rail column 42 wide with the tools centred, the swatches 18 points below the last tool (Swift's spacing of 10 and padding of 8). SwatchButton draws every swatch: the palette's (radius 6, white ring 1.5), the Brush bar's (34 by 18, radius 4, white ring 1, black rim), the Shape bar's Fill and the Type bar's (36 by 18, radius 3, a half-black rim). The Brush bar's shows the foreground for the Brush and Spot Healing, not Clone Stamp, the Smear or a mask, resting while the palette cannot be edited; the Type bar's shows typeColor and opens openTextColorPicker. X and D swap and reset the palette on the canvas and in the list, Shift allowed, not with Ctrl, Alt or Meta; ProjectWorkspace::canSwitch refuses while a picker is open. Tests wait for the panel to take the keys (QApplication::activeWindow): a tool window reads as active whenever its parent is, and offscreen a hidden panel stays the focus window, where a window manager hands the keys back. Equivalent mutants: raise (offscreen keeps no stacking order), refocus's isVisible (offscreen activates no hidden window), show's wasVisible guard (a shown panel stands where it was remembered), and in sampleCompositeColor Swift's min(alpha, value) (a premultiplied channel never passes its alpha), painter.end() before the read (the raster engine writes at once) and the warning.
  • The Eyedropper and picking (8.10b): EditorCanvas+Picking.cpp holds Swift's sampling. palettePicking is the Eyedropper, or Alt with the Brush, Spot Healing or the Gradient while no stroke runs and no gradient end is dragged; picking adds an open picker (the Levels and hue sample modes arrive with 9.1 and 9.3). A press while picking, Space aside, samples at once and keeps the colour it began from; a drag samples on; the release ends it and, with a picker open, hands the picker the keys back (ColorPickerPanelController::refocus, which the canvas includes, as Swift's calls it). sampleColor samples into the open picker, else sets the foreground through setForegroundColor while the palette can be edited (a chosen mask too, as Swift sets foregroundColor), and rings the point. Qt can lose a release: a move without the left button, a lost focus or the next press ends a sampling. Picking outranks every tool's press, a double click (the second click is a press, so the inline editor and a polygonal Lasso stand aside while the picker is open, Swift's hitTest), a hover with no button held (the eyedropper alone: the brush circle neither moves nor shows, as Swift's mouseMoved returns first; a right-button drag still sizes the tip, as Swift's rightMouseDragged ignores picking) and the cursor (after the drag cursors, before Space). syncPicking ends a sampling when picking begins or ends: synchronizeDisplay runs it, as does every modifier change (Swift's monitor), a sampling press before it begins (Alt may first show in the press, and the foreground's signal would otherwise end the new sampling) and every move (Alt let go unseen). pickingRelease returns nothing, where Swift returns from mouseUp: a sampling press begins no other gesture, so the rest of the release finds nothing to end. SampleRingOverlay is no widget, like BrushCursorOverlay: 116 points round the pointer, a 24-point ring of gray 0.45 under a 16-point one, the sampled colour on the top half and the original below, each half replacing the clip and one restore dropping it, drawn over the overlays and under the inline editor; showsSampleRing (a getter and a setter that notifies) hides it. The Eyedropper's bar is Swift's inline one, built in ContentView: the title and a "Sample Ring" QCheckBox 16 points on, following the session under a QSignalBlocker. eyedropperCursor draws the symbol into Swift's 20-point box of a 24-point image, sixteen white offsets of 1.25 and then black; its hot spot is the drawn tip, (5, 19), as Swift's (3, 21) is its symbol's tip. The glyph is Rendering/EyedropperIcon, which the rail and the cursor share, as they share SelectionIcons. A mounted canvas leaves the keys to an open picker (Swift's viewDidMoveToWindow). The application event filter moved, unchanged, beside the modifier helpers in EditorCanvas+Cursors.cpp, keeping the navigation file under 500 lines. Equivalent mutant: the ring's setBrush(Qt::NoBrush) (the painter reaches it with no brush).
  • Levels in the session (9.1a): Document/Levels.h/.cpp twin Swift's file, Levels+Session.h its session half; LevelsAutomatic.h/.cpp and LevelsAutomatic+Session.h the other file. LevelsPixels.c compiles in place beside the other kernels. LevelsChannel, LevelsSample and LevelsAuto keep Swift's raw values (rawValue) and case order (allLevelsChannels, allLevelsSamples, allLevelsAutos); LevelsSample is declared opaquely in Levels.h, since LevelsEdit holds one, and defined with the automatic levels, as Swift's. LevelsSettings::sampling is declared in Levels.h and defined in LevelsAutomatic.cpp, as Swift's extension; settings(mode, histogram) stands for LevelsAuto.settings(histogram:); setCurrent is Swift's current setter, storing the range normalized. A histogram is LevelsHistogram, four arrays of 256 bins where Swift has [[Double]], and LevelsHistogramDisplay::scale takes one channel's bins. LevelsFilter::run leaves soft edges to the kernel. Swift's cc5aac4 once unpremultiplied each soft edge before levels_apply, which unpremultiplies again; Swift agrees with the twin since 1.2.0 (f6a74c9 removed that pass), and its inputClippingGammaOutputInversionAndAlpha holds (64, 96, 128, 128) as the twin does. The histogram hands the kernel one row at a time with the mask's own row, since Qt pads a gray image's rows and the kernel accumulates; neither it nor the filter's tables take heap memory of their own, whose failure would throw std::bad_alloc past the worker's catch. LevelsEdit is a plain class in a std::optional of the session, like FilterEdit, its id standing for Swift's object identity; past 8000 pixels a side its preview source is a scaled copy (sides truncated, one pixel at least), drawn from a painted layer's tiles without flattening them. The session keeps a watcher each for the histogram, the preview and the commit, with the edit each serves: where Swift cancels a task, the twin forgets whose it is (m_levelsPreviewFor; a histogram needs no token, since a new edit's count replaces the watcher's future, whose signal Qt then drops), so a result for a turned-off, closed or committing edit drops; one preview runs at a time, the newest job waiting, and a commit drops the waiting job too. A run counts as running until its finish has run (m_levelsRendering), as Swift's task clears itself only after its await: Qt's setFuture drops a replaced future's pending signal, and isRunning and isFinished both end with the worker (probed on 6.4.2). A histogram or preview that fails logs on omaphoto.app where Swift's try? is silent; a failed histogram leaves the bins empty and ready, as Swift's. setLevelsSampleMode is the sheet's write (Swift's sheet sets sampleMode and bumps brushRevision itself); it notifies. The commit writes the layer in place and drops shape and text, as the filter's does. beginLevels leaves out Swift's levels == nil and its activeLayer and document guard (canAdjustColors reads all three); hueSaturation arrives with 9.3, cancelCrop with 10.1, the adjustment layer hooks of updateLevels, cancelLevels and commitLevels came with 9.5c. The gates, canAdjustColors and canStartProjectOperation read levels, selectLayer and selectTool refuse while it is open, and closing it resumes file requests, Swift's didSet; the commit's end resumes them through setIsProjectBusy(false), after the edit is gone. commitLevels hands its worker the asset, so a painted layer flattens there, where a failure is caught (Swift's lazy image flattens as its task draws). Left out as dead: the display scale's zero-peak guard (with no bin counted the interior is empty too), the preview flag in previewImage and in a landing preview (turning it off clears the preview and forgets the run), and in the automatic levels contrast's low < high (each channel's low lies below its high, so theirs do), neutral's 0 < mean < 1 and its 9.99 cap (weight at both ends of a range keeps the mean a thousandth inside, so gamma stays under 9.97), and the sample's 0 and 255 clamps (a sample is 0 to 1). Equivalent mutants: the white sample's max(black + 1, …) (normalized keeps white above black), sampleLevels' bounds on the layer's own pixels (a pixel off the image draws clear, which the alpha test refuses), the histogram's committing check (a count finishing during a commit, a race no test orders), the commit's check that the layer kept its pixels and place (nothing edits a layer while the project is busy), the tables' reservation and the warnings. LevelsFailureTests starves allocations through AddressSpaceLimit in a binary of its own: a canvas-wide stroke leaves hundreds of MB free inside malloc's arenas, which malloc_trim cannot unmap, so its painted layers are rasters built directly; worker threads allocate within their own reserved arenas, so only asks over 64 MB fail there.
  • The Levels sheet (9.1b): UI/LevelsSheet.h/.cpp twin Swift's sheet; Histogram, Gradient, Track and Handle are private widgets of that file, and the histogram and the tracks follow the session's changed() themselves. Each handle is a 22 by 20 widget of its own, a child of the sheet placed over its track, as Swift's ForEach makes a view each: a later handle lies on top (white over gamma over black), a press drags only its own, each carries Swift's label, and at either end half of it lies in the sheet's padding, where a child of the track would be clipped. The output gradient is made before the output handles, so they lie on its last row as SwiftUI stacks the later view. A drag reads the pointer along its track, Swift's named coordinate space. The triangle is a path (apex 3.75, base 14.25, 12 wide, Swift's 12-point triangle.fill); its half-point gray shadow is a gray rim, since Qt has no blur. The histogram's colours are SwiftUI's gray and, from Apple's HIG for macOS 26 (the Swift target), its dark red, green and blue: (142, 142, 147), (255, 66, 69), (48, 209, 88), (0, 145, 255), data colours like the canvas's grays, whatever the theme. The entries are the picker's PickerFields: a number applies on Return or on leaving, as Swift's value binding, other text goes back, and an entry keeps its typing until then, unless its own handle is dragged, which takes no focus and replaces the typing with its value (9.6f); numbers are read and shown in the widget's locale with Swift's decimals, as Swift's .number format follows the user's (a German gamma reads 2,5; no level reaches a thousand, so nothing is grouped). A click on OK takes the focus first, so a typed entry applies before the commit, where AppKit's buttons take no focus and the typing is lost; Return in an entry goes on to OK, the panel's default, as in the picker. The eyedroppers are checkable buttons set from the session, the chosen glyph in the accent and the others in the secondary ink, for Swift's .tint, redrawn on PaletteChange. The channel picker is named by its label (setBuddy): on Linux Qt names a combo box by its label relation, never by accessibleName. Alt+P is the Preview box's own shortcut in the panel and the canvas's key when the canvas has the keys. For an adjustment layer's editor the caption says the underlying pixels (9.5d). ContentView+Panels.cpp holds synchronizePanels, Swift's onChange(of: levels == nil), and since 10.1 the importer and the alerts, because ContentView.cpp runs to 500 lines: a FloatingPanel named levelsPanel shows a new sheet when an edit opens, its close cancelling as Swift's onClose, and hides when the edit closes. The canvas draws previewImage in the layer's place after a filter's; a press in a sample mode is sampleLevels and hands the panel the keys (FloatingPanel::refocus); other presses wait for Space, the Hand or the Zoom while Levels is open; Escape, Return, Enter and Alt+P are its keys and every other key but Space passes on; picking reads the sample mode, so the eyedropper cursor shows; the application filter's brackets rest. ContentView::acceptsDrop needs no term of its own: canSwitch reads canStartProjectOperation, which reads levels. The Image menu's Levels… is Ctrl+L and follows canAdjustColors (its hueSaturation term arrives with 9.3); Import Images and the New canvas button rest while Levels is open. Undo and Redo are bare, since Levels closes the history, and enabled only while a field has focus, as for a busy project: Swift's bare entries act only on a text view. Equivalent mutants: the histogram's zero-peak guard and its max(0, …), each standing in for the other (with no bin counted, 0/0), its min(1, …) (the widget clips a taller bar) and the bars' + 0.1 (Qt covers abutting rectangles of one path exactly); the drag's clamp of the tone, white's max(black + 1, …) and gamma's min(0.999, …) (normalized does each); either of a track's move and resize events alone (a layout moves and sizes it together); the canvas's !preview before the Levels preview (a filter edit and Levels never overlap); passing an unhandled key on (nothing above the canvas takes keys) and the base changeEvent (the sheet paints nothing of its own). Qt sizes the loading note itself: a shown child without a layout is adjusted to its hint.
  • Curves and the image adjustments (9.2): Document/Curves.h/.cpp and Document/ImageAdjustments.h/.cpp twin Swift's files, UI/CurvesControls.h/.cpp Swift's view. AdjustPixels.c was already compiled for the downsample cache; Curves and Exposure run levels_apply from LevelsPixels.c, as Swift's do. CurvesSettings.channels is a vector of point vectors, as Swift's array, so isValid still counts four. value finds Swift's lastIndex(where:) with a loop; a NaN input takes the first segment at t = 0, as Swift's max(0, .nan) gives 0. ImageAdjustmentPixels::run hands its body the premultiplied RGBA context BrushRaster::context makes and returns that context. AdjustmentColor keeps Swift's two initialisers as constructors; the palette's has its callers since 9.4a (a Gradient Map starts from the palette, its picker) and gains 9.5's (new layers), as normalized serves the filter's settings since 9.4a and the adjustment's with 9.5; ends, which apply reads, gains the filter sheet as a reader with 9.4b and returns an Ends struct for Swift's tuple. GrainSettings::apply takes an optional seed that replaces the stored one and hands the same image back when the amount is 0. Left out as dead: the isFinite terms of AdjustmentColor.isValid and of the curves' points (the ranges refuse NaN and infinities), and the points' x range (ends at 0 and 255 with x strictly rising keep every x inside). CurvesControls takes Swift's binding as a value and a change function and shows the settings on synchronize, which each of its own edits calls and its owner will (9.4's filter sheet); a channel shown anew drops the selection and the drag, Swift's onChange. The graph (curvesGraph) is a private widget 260 tall: black at 35%, white quarter lines at 12%, the curve two wide, 8-point dots with the chosen one in the accent; presses and drags follow Swift's gesture (a point within 14 curve units is taken, else one is added between x 1 and 254, more than a unit from the others, 32 at most; an inner point keeps a unit from its neighbours; the left button alone). Remove point keeps Swift's guard (an inner point that is still there), so a selection another change has left behind removes nothing. The readout (curvesReadout) truncates as Swift's Int(…); SwiftUI's monospacedDigit has no Qt 6.4 twin (font features arrive in 6.6), so the digits are the font's own. Carried from 9.1b's review: the Type bar's size and tracking read and print through its locale, as Swift's .number fields, without group separators, as every field of the twin; leading keeps Double(text)'s period, as Swift's. Equivalent mutants: the curves' 2… lower count (one point cannot hold both ends), < for <= in the segment search (the curve is continuous at its points), < 0 for <= 0 in the inner slope (a zero slope's reciprocal is infinite, so the harmonic mean is zero anyway) and the final 0…255 clamp (Swift's slopes never overshoot a valid curve); Exposure's max(0, …) before the gamma and its table's lower clamp (a negative light ends at zero either way), the gradient map's byte clamp (its colours lie in 0…1); the controls' x > 1 and x < 254 (a point more than a unit from both ends lies inside them) and Remove's upper bound (Remove rests on the last point).
  • Hue/Saturation in the session (9.3a): Document/HueSaturation.h twins Swift's types. Swift's file runs to 574 lines, so the twin splits it: HueSaturation.cpp (ranges, bands, sample modes, settings, the edit), HueSaturation+Filter.cpp (the cube) and HueSaturation+Session.cpp (the session extension, canAdjustColors with it; HueSaturation+Session.h its fragment). ColorRange and HueSampleMode keep Swift's raw values and case order (allColorRanges, colorRanges, allHueSampleModes); defaultBand is a free function where Swift has a property, and help gives the modes' help; their SF Symbols, the eyedropper and its badges, are the sheet's to draw (9.3b). HueSaturationSettings keeps Swift's dictionaries as std::maps, so the constructor's entry for its range and the setters' inserts compare as Swift's do; Swift's computed hue, saturation, lightness and band are getter and setter pairs. HueSaturationFilter is a namespace; HueResponse and Color stand for Swift's tuples, and cube returns the floats Swift packs into Data. Core Image's CIColorCube becomes a CPU lookup, rows in parallel: each pixel unpremultiplied, read trilinearly from the 33-point cube, premultiplied again and rounded once; Core Image's own precision may differ by a level, and Swift's tests allow eight. A failed table or row allocation is ExportError(render), since a worker's bad_alloc would pass its catch; HueSaturationFailureTests reaches that net through a picture one pixel wide, whose row list asks as much as its pixels. A selection blends the result over the job's own pixels, as the Levels filter does, where Swift draws them into a context first: the same bytes, since layer pixels are that context's format. HueSaturationEdit is a class in a std::optional, as LevelsEdit, its id standing for Swift's identity; its preview source is made as the Levels edit's (to 8000 pixels a side, from a painted layer's tiles without flattening). The session keeps a watcher for previews and one for the commit. Where Swift cancels the preview task (Preview off, nothing to show, OK) the twin forgets whose it is, so that result drops; a result for a closed edit fails the id check, Swift's hueSaturation === edit. A run counts as running until its finish has run (m_hueSaturationRendering, as Levels' queue), so a failure is reported even when the next job waits. Known difference: Swift's run for a closed edit returns before it looks at the queue, so a job queued behind it waits for the next change; the twin's runs it. Known difference: Swift's task never looks at its cancellation, so a render still running when Preview goes off, or when OK is pressed, lands. A failed render sets brushError whichever edit asked, as Swift's adjustedPixels does. Known difference: Swift's beginHueSaturation resolves a pending gradient without waiting, so its edit holds the pixels from before the gradient and the commit, finding the layer changed, lands nothing; the twin waits for the gradient, as beginLevels does. commitHueSaturation refuses while a commit runs (Swift has no guard; its second commit would find the layer changed and land nothing), keeps a copy of the edit for the finish (CommittingHue), frees the project before the edit goes (Swift's two defers) and names the new asset after the layer; the rebuild drops shape and text. sampledHue reads the composite through PickerHSB, as Swift's; beginHueTargeting takes Swift's max(by:), the first of the strongest claims. setHueSampleMode and setHueTargeting are the sheet's writes and notify on a change; endHueTargeting changes nothing observable and stays silent. Swift's beeps are warnings on omaphoto.app. Left out as dead: beginHueSaturation's document and activeLayer guard (canAdjustColors reads both) and the targeting's ?? .reds (the ranges are never empty). Equivalent mutants: the weight's fallbacks for ramps of no width (no hue lies inside one) and its <= at the plateau's end (both sides give 1); toHSL's cap on saturation, toRGB's gray return and clamp (the arithmetic stays within 0 and 1 but for rounding, which later clamps absorb); a clear pixel's fill (its lookup is multiplied by alpha 0); the response's skips of a still range or a zero weight (each adds nothing); the saturation's floor of zero (toRGB draws gray at or below it); the amount's clamps and the lightness clamp (a sum past ±100 reaches white or black either way). The hooks: canEditLayers, canContentAwareFill (so beginFilter), beginLevels and ProjectWorkspace::canSwitch read hueSaturation; canUseHistory, canStartProjectOperation, selectLayer and selectTool do not, as in Swift.
  • The Hue/Saturation sheet (9.3b): UI/HueSaturationSheet.h/.cpp twin Swift's file, HueSaturationSheet and SpectrumEditor; the editor takes Swift's binding as a value and a change function, as CurvesControls does. The sheet writes through Swift's settings binding (change: the settings as they stand, the defaults once the edit is gone, handed over with the edit's Preview). The range picker is a 160-wide QComboBox, resting under Colorize; Swift hides its label, so a hidden QLabel names it through the label relation Linux reads (there a combo box's own name is its text). Since 12.14c the sliders are Camera Raw's coloured ones over a thousand steps, writing whole numbers as Swift's do, and show the session's value through reshape, clamped to their range, so a typed value past it rests the slider at its end without writing back. Each field is a PickerField: a readable, finite number applies when the field lets go of focus (Swift's value binding; Swift's Int() traps on the rest, which revert), and Return clamps it to the slider's range (Swift's onSubmit), then goes on to OK, the panel's default button, where Swift's handler commits by itself. The field's own slider replaces its typing with the value it sets (a wheel leaves the keys with the field; 9.6f); the others leave it. The fields print whole numbers through their locale, ungrouped as every field of the twin; the unit sits two points past each, the hue's a degree sign, the others empty, as Swift's unitSuffix. The eyedroppers and the targeting button paint themselves: a quarter of the accent in a rounded frame when chosen (Swift's plain button with its background), the eyedropper through EyedropperIcon at 14 points with an 8-point disc cut by a plus or a minus (Swift's plus.circle.fill and minus.circle.fill), and hand.point.up.left as a finger and a palm outlined and turned; HueSaturationSpectrumTests pins each glyph's ink, measured once. The spectrum paints Swift's 72 slices, the after bar through shiftedHue, clipped to a rounded rect of 3; its handles are bars for the core and blocks for the shoulders, dragged by nearness round the circle (Swift's nearestHandle); the held handle stays until a press starts afresh, because Qt can lose a release, and Swift's width > 0 is left out (a press needs width). "Limited to the selection" shows with a selection, never for an adjustment layer's editor (Swift's adjustmentOriginal term, 9.5d). ContentView::synchronizePanels shows the sheet in adjustmentPanel, titled Hue/Saturation, while an edit is open; its close button and Escape cancel. On the canvas picking counts the hue eyedropper: a press samples the range and hands the panel the keys, unless the colour picker is open or palette picking runs with no range eyedropper armed, which sample colour, in Swift's mouseDown order. Targeting shows Qt::SizeHorCursor (Swift's resizeLeftRight) after picking and before Space; a press begins it and ends there even when refused (Swift returns), a drag moves it by the pointer's x from the press, Ctrl for Cmd driving the hue, and sets the resize cursor again, as Swift's does on every drag, so Alt's eyedropper under the Brush lasts only until the next, a release ends it, and a move without the left button ends a drag whose release was lost; with Space the press pans. A double click with targeting armed is a second press, ahead of the Type tool's words and the polygonal lasso's close, as Swift's mouseDown sees it: a text draft and a polygonal outline can both be open under Hue/Saturation (Swift's canAdjustColors asks for neither). The press, drag and release live in EditorCanvas+Picking.cpp beside the sampling. The canvas draws the edit's preview in the layer's place after a filter's and Levels'. The Image menu's Hue/Saturation… (Ctrl+U) follows canAdjustColors, so it stays enabled while its own edit is open, as Swift's (choosing it again changes nothing); Levels… rests meanwhile. In a text field, on a desktop whose keyboard scheme binds Ctrl+U to deleting the line (X11's), the field takes it first, as with Ctrl+A; offscreen binds nothing, so the tests see the menu. Equivalent mutants: the top row's spacing (its stretch takes the room), the slider's stretch (a horizontal QSlider expands by itself), nearestHandle's fmod (Swift's too: the handles and the pointer lie within one turn) and endHueTargeting on release (the canvas never reads the drag it clears). A buttonless move never reaches the handles (Qt drops it without mouse tracking), so their left-button check is tested with the right button held.
  • Filters in the session (9.4a): Document/Filters.h/.cpp hold Swift's model (Remove Background's kind, its four settings, SubjectRemoval and commitBackgroundMask since 9.7); isImageAdjustment and the list of kinds arrive with the Filter menu (9.4b), their only reader. FilterSettings::normalized clamps through ImageAdjustmentPixels::clamp, a value that is not finite taking the default. PixelFilter::run hands Curves, Exposure, the Gradient Map and Grain to their settings' apply (Grain in layer pixels, 1 / scale units, the job's seed), the fill to ContentFill::run, and the rest a premultiplied copy, as Swift's contexts: Gaussian Blur is PixelAdjust::gaussianBlur unclamped, clear past the edge as Core Image blurs a finite image; Motion Blur is PixelAdjust::motionBlur, CIMotionBlur on the CPU: Gaussian taps (exp(−i²/2σ²) over three sigmas, normalised) along the angle at σ = distance × scale / √12, each tap read bilinearly with clear past the edge, rows in parallel, the angle counterclockwise on screen since Core Image's y points up; Add Noise and Lens Correction run Swift's kernels, NoisePixels.c and LensPixels.c, compiled in place (the lens's k is distortion / 100 × 0.35). Known difference: Swift measured Core Image's taper on a dot; the twin's is the exact Gaussian. FilterEdit::prepare is Swift's prepared and prepare in one: the mapping and a preview copy at most 2048 on its long side (sides truncated, one at least; whole for noise, grain and the fill; a painted layer's copy drawn from its tiles, a full-size one flattened as Levels' is). growForBlur pads by the blur's reach (3σ + 2, or half the streak + 2) through grow, 30,000 a side and one surface at most (DocumentLimits, 200 megapixels since 12.11a; ProjectError(tooLarge)), and never shrinks the grid; its rounding up is left out, since grow takes the integral box. The session keeps filterSettings, which each commit writes and each filter starts from, the Gradient Map from the palette's foreground and background instead. updateFilter(settings, preview) normalises, grows the grid when a blur needs more room (a failure is brushError; the old preview goes), renders an automatic kind once per settings (preparedSettings, which the worker hands back with its image), and with Preview off drops the waiting job and the preview. Known difference: one watcher serves every edit, so a job may wait behind a cancelled edit's run; that job is Swift's running task, and Preview off leaves it (dropped, it would leave preparing set for good). commitFilter closes a Gradient Map picker with OK first (cancelFilter with Cancel), closes as Cancel does with no step when nothing would change (no distortion, Exposure's defaults, no grain), forgets the running preview and drops the waiting job (Swift cancels the task), and hands the worker the job's parts, so a painted layer flattens there; a blur's result is trimmed to the pixels it left, any other keeps the grown grid, and an automatic kind reuses its preview when the settings match. Known difference (a fix): Swift carries a covering mask over the grown box while a trimmed blur's pixels sit on the trimmed box, so the mask slid and squeezed; the twin carries it over the layer's new box. The picker's gradientMap target (ColorPickerTarget::highlights names the end; the title follows it) opens on the open Gradient Map through openGradientMapColorPicker, previewGradientMapColor writes the working colour into the map through updateFilter (silent when nothing changes), and closeColorPicker writes OK's colour or puts the original back, the palette untouched. Left out as dead: the opener's committing term (a commit keeps the project busy, which canEditPalette reads) and setGradientMapColor's (a commit closes the picker first); that function's other guards came back with 9.5c. PixelFilter::trimmed throws ExportError(render) when its crop finds no memory, since Swift's cropping shares the pixels and cannot fail so; the blurs' row lists (rowNumbers) turn bad_alloc into ExportError(render), as Hue/Saturation's do, and their weight tables stay plain, the callers bounding them to 1,734 entries. Tests: FilterTests (Swift's four; the Gaussian one tests the code, which grows and trims, as Swift's has since 1.2.0, 7c4512a), FilterModelTests (Add Noise and the lens checked against Swift's kernels, whose headers only that test includes), FilterSessionTests, FilterFailureTests (a binary of its own, under AddressSpaceLimit), and in ImageAdjustmentTests Swift's two Image-menu tests and the command half of curvesIdentityAndImageCommandPreserveAlpha (Swift's AdjustmentLayerTests holds it). Equivalent mutant: >= in growForBlur's guard (at equal reach it grows the same grid again).
  • The filter sheet (9.4b): UI/FilterSheet.h/.cpp twin Swift's file, FilterSheet and GradientMapControls. The sheet is the content of a FloatingPanel named filterPanel, which ContentView::synchronizePanels shows with the edit, titled by its kind, and closes with it, its close button and Escape cancelling (Swift's onChange(of: filterEdit == nil)). The kind's controls are made once, since an edit's kind never changes. Swift's control is a title (sixty points at least, the slider's buddy), a slider over a thousand steps of its range, or of the range's logarithm where Swift's is logarithmic, the value rounded to the field's decimals as Swift's binding rounds it, and a 56-point PickerField with its unit two points after (Swift's unitSuffix), which applies a readable finite number on Return or on leaving (normalizing clamps it), gives up its typing when its own slider changes the value (a wheel leaves the keys with the field; a notch that rounds to the same value changes nothing; 9.6f), and shows Swift's .fractionLength(0...decimals): at most that many decimals, none trailing, in the field's locale, digits never grouped, as in every field of the twin. The sliders show the session's number under a QSignalBlocker, so a dragged thumb sits at the rounded value, as Swift's binding shows it; a step that rounds to the setting it already holds changes nothing, so the thumb keeps its place and arrow steps add up. Add Noise's distribution is two checkable buttons in a QButtonGroup beside its label (Swift's segmented picker), Monochromatic a QCheckBox; Lens Correction's note is callout-sized in PlaceholderText; Curves hosts CurvesControls through the settings. GradientMapControls draws Swift's gradient bar (20 high, rounded 4, a 35% black rim inside), the ends as the palette's SwatchButtons (24 points, radius 6, a white ring of 1.5 inside a black rim) with their titles, help and accessible names, and Reverse; a swatch opens the picker on its end, and on every change of the picker's colour the sheet calls previewGradientMapColor (Swift's onChange), which ignores a palette picker. The controls wait for the sheet's first synchronize, which runs once they are built. settings and update keep Swift's fallbacks for a sheet whose edit has gone: the defaults, Preview on. The Image menu gains Curves… (Ctrl+M) before Levels…, and Exposure…, Gradient Map… and Grain… after Hue/Saturation…; the Filter menu (Fil&ter, between Image and Layer, as Swift declares it) lists allFilterKinds but the fill and the image adjustments (isImageAdjustment), Remove Background among them since 9.7; every entry rests while colours cannot be adjusted or Hue/Saturation is open, as Swift's. Tests: FilterSheetTests (7.5's, moved to the panel), FilterControlsTests (each kind's controls, the sliders' travel, the fields, the Gradient Map's swatches, bar and repaint), the menus' in SelectMenuTests and CompositorMenusTests. Equivalent mutants: Swift's ?? .gaussianBlur for the kind (the panel makes a sheet only for an open edit) and the memory of the picker's colour (previewGradientMapColor with an unchanged colour changes nothing).
  • Adjustment layers in the document and the store (9.5a): Document/LayerAdjustment.h/.cpp twin Swift's file, LayerAdjustment+Session.h its session half. AdjustmentKind keeps Swift's raw values (rawValue, and adjustmentKind(text) for the manifest) and case order (allAdjustmentKinds); filterKind came with 9.5c, its reader; symbol names an SF Symbol, whose glyph the panel draws (9.5d). LayerAdjustment keeps Swift's fields: the legacy Hue/Saturation four, the optional hsvSettings, Levels and Curves, and the optional Exposure, Gradient Map and Grain, whose computed properties are getter and setter pairs; resolvedHSV falls back on the legacy four. isValid keeps Swift's bounds (hue within 360 either way, the rest within 100, each range's values alike, every band's handles finite, Levels as normalized, then every kind's own rules whatever the kind) and leaves out the isFinite terms the bounds already refuse and Levels' count (the twin holds four ranges). apply arrives with 9.5b, its reader. ImageLayer.adjustment joins equality and travels wherever Swift lists it: the record (hierarchyRecord, so the snapshot), ImageLayer(record, …) (so an install and a resize's rebuild), both resizers and duplicates. A copy between projects needs no term of its own: the baker bakes nothing for a layer without pixels, and the placement unlinks it, where Swift skips the bake. The manifest's adjustment is Swift's synthesized Codable, written by hand in IO/ProjectStore+Adjustment.cpp over the readers the manifest shares (ManifestJson in IO/ProjectStore+Json.h): every key required but the four optionals, which stay out when absent; a ColorRange-keyed dictionary is an array of key and value in turn, since Swift's ColorRange declares no CodingKeyRepresentable (a range Swift does not name refuses it, and so does an odd count, whose last key has no value to read, so Swift's count check has no twin; a later pair wins, as a dictionary assigns); the seed is a UInt32. The store refuses an adjustment before version 7, on a folder, with pixels or invalid; Swift's text rule's adjustment term is dead, since text needs pixels. LiveMaskGraph::validate refuses clipping to an adjustment layer, so canLinkMask refuses one as a source; canPaint refuses an adjustment layer's pixels, not its mask. addAdjustment puts a blank layer named by its kind just above the active one (into an active folder, uncollapsed; Swift's addBlankLayer alone lifts a layer above the folder's contents), as one New … Adjustment step, a Gradient Map running from the foreground to the background and each Grain with a seed of its own, 10,000 layers at most, then asks for its editor (Swift's adjustmentEditingID, 9.5c). updateAdjustment writes valid settings alone, with no step of its own, and moves brushRevision. Tests: LayerAdjustmentTests (kinds, validity, the optionals, the manifest's keys and refusals, the store's and the graph's rules, adding, updating, travel), AdjustmentLayerTests (Swift's legacyHSVStillDecodesAndRenders; the rest arrive with 9.5b and 9.5c), Swift's settingsSaveAndOlderAdjustmentsStillOpen in ImageAdjustmentTests, and an unbaked copy in ProjectCopyTests.
  • Adjustment layers drawn (9.5b): LayerAdjustment::apply runs the kind's own filter (Hue/Saturation and Levels through their filters, Curves, Exposure and the Gradient Map through their settings), Grain placed by the region it is given: its origin and its units a pixel, region.width / image.width, so the export and drawLiveComposite keep the pattern in the document however their drawing is cut or scaled. Known difference, Swift's too: the canvas places layers by scale and centre, not through the painter's transform, so its region is the view's and Grain's pattern follows the view, moving under a pan and differing from the export (grainFollowsTheViewOnTheCanvas). LiveMaskRenderer gains Swift's three hooks as public members: adjustment, adjustmentOpacity and adjustmentClip, which multiplies the layer's own mask into device coverage where Swift clips the context. prepareStacks keeps every layer's mode (Swift's blendMode); Swift's adjustment(base) == nil there is dead and left out, since nothing clips to an adjustment (LiveMaskGraph). drawComposite hands an unclipped adjustment to adjust and draws a clipped one only inside its base's stack, where adjust runs on the group after its unpremultiplied base, as Swift's. adjust reads the painter's device (a QImage, as LayerRenderer requires; else a logic_error), converted to the kernels' RGBA8888_Premultiplied at a pixel ratio of one, within the pixel budget (past it, a warning on omaphoto.rendering, where Swift's try? is silent), applies the settings over the region the device shows (its device transform inverted), blends a mode other than normal at full coverage and restores the original alpha (layer_extract_alpha, layer_unpremultiply_opaque, LayerRenderer::composite for the mode, layer_restore_alpha), then mixes the result into the original by opacity × mask × outer clip, rounded once, where Swift mixes by opacity (CIBlendWithMask) and then draws through the soft clip, and draws it back one to one in Source mode under the painter's own clip, as Swift's copy honours the context's clip, the painter's whole device transform cancelled as the stack group cancels it, so an image of any format or pixel ratio takes it (the app hands it only RGBA8888_Premultiplied at a ratio of one: the export's image, a stack's group, AdjustmentSurface's surface; a caller of the renderer itself may hand it any image). A failure to allocate is ExportError, caught there as a warning; anything else is a bug and is left to crash. ImageExporter::render, drawLiveComposite and the canvas set the hooks as Swift's three places do; drawLiveComposite gains Swift's onSurface and, when a document holds an adjustment, draws through AdjustmentSurface, so a painter's opacity or clip applies to the composite once. The canvas clips an adjustment by its enabled mask, mid-stroke by the edit's tiles (maskEdit, shared with the folders' clip, Swift's liveMaskEdit), placed by the layer's transform at the view's scale; its DisplayState counts adjustment layers with their settings, since an undo restores settings without moving brushRevision. Tests: Swift's AdjustmentLayerTests (the curves command test moved there from ImageAdjustmentTests, its Swift home), theLiveCompositeDrawsAsTheExport, opacityMixesAndGrainSitsInTheDocument, everyKindAdjustsThroughItsSettings and aFoldersMaskClipsTheAdjustmentsInside beside them, anyImageTargetTakesTheAdjustment beside them, anAdjustmentAdjustsWithinItsBudgetAndStack and anAdjustmentTakesAnyImageTarget in LiveMaskRendererTests, adjustmentsShowAsTheExportDoes, anAdjustmentsMaskClipsItOnTheCanvas and grainFollowsTheViewOnTheCanvas in CanvasDisplayTests.
  • Editing adjustment layers in the session (9.5c): Document/AdjustmentEditing.cpp twins Swift's file, AdjustmentEditing+Session.h its session half; adjustmentEditingID (a getter and a setter that resumes file requests and notifies, Swift's didSet) and adjustmentOriginal (a getter) sit in EditorSession.h. addAdjustment asks for the new layer's editor last, as Swift's. beginAdjustmentEditing takes a completion callback: it hides every layer not beneath the adjustment in hierarchy order (the adjustment itself and all above; the records stay for live masks and folders), renders that snapshot through ImageExporter::render in a worker (QtConcurrent::run with a watcher per call, as each Swift call is a task of its own; the worker catches ExportError alone, since a session's snapshot never fails the store's rules), and on the UI thread opens the kind's editor on a layer of that image at the origin: Levels with its histogram (countLevelsHistogram, which beginLevels shares), Hue/Saturation with resolvedHSV, or the filter panel of filterKind (declared in LayerAdjustment.h, as Swift's extension) with the four settings; then it keeps the original and opens one Edit … Adjustment transaction. The editors' layer has an id of its own, so the canvas never shows their previews, and none renders: updateLevels, updateHueSaturation and updateFilter hand the settings to previewAdjustmentEditing first (the edited settings, or the original with Preview off) and notify, since settings the layer refuses still change the editor; the three cancels and commits hand the edit to finishAdjustmentEditing first (a filter's after its Gradient Map picker closes, as in Swift). finishAdjustmentEditing writes the edited settings on OK and the original on Cancel, closes the three edits and the Hue tools (sample mode, targeting, a targeting drag), ends the transaction, clears the request, resumes file requests, asks for canvas focus and moves brushRevision. OK writes the settings out even unchanged, so an Exposure or Hue/Saturation layer made without them gains them in a step, as in Swift. A Gradient Map picker stays open when finishAdjustmentEditing is called directly (Swift's tests call it; the app's cancel and commit close the picker first), so setGradientMapColor keeps Swift's guards: with no filter open, or another kind, it writes nothing; its committing term is dead, since a commit closes the picker before it begins. Left out as dead: Swift's editedAdjustment returns nil when its kind's edit is gone, which never happens while adjustmentOriginal is set (the two are set and cleared together, and every path that closes an edit reaches finishAdjustmentEditing first), so the twin reads the edit through value(); the ?? .curves behind filterKind, a checked read; finishAdjustmentEditing's cancellation of previews (none runs for an adjustment's editor, and a histogram lands only on a Levels edit still open) and its hueSaturationPending = nil (only a preview sets it). Swift's .task(id:) cancels a render whose request changed; the landing's guard (the same request, no editor open) does that work here, and nothing in the app changes the request while a render runs (every writer sits behind canEditLayers). A fix beyond Swift: Swift's landing checks the request alone, so a Levels, Hue/Saturation or filter edit begun on other pixels while the render ran (choosing a layer and opening one stay possible, as in Swift) would be replaced or joined by the adjustment's editor; the twin's landing leaves that edit alone, clears the request, resumes file requests and logs a warning on omaphoto.app. A render or an editor that fails clears the request and sets brushError, as Swift's catch. canEditLayers and canStartProjectOperation read the request; canUseHistory does not, as in Swift, so undo stays open while the render runs, and an editor may open for a layer undone meanwhile, whose close writes nothing. Swift's isGroup != true there is left out as dead: a folder not beneath the adjustment holds nothing that is, and a live mask's source is never a folder. Equivalent mutants: the start guard's request term (the landing asks again; the start's only spares the render) and its adjustmentOriginal term (the original never outlives its kind's edit, which the next terms read). Tests: AdjustmentEditorTests (Swift's sharedEditorsKeepPixelsDynamicAndSupportCancel for every kind, then what lies beneath, the refusals, a failed render, an edit begun meanwhile, the gates and file requests, the Hue tools, a layer undone meanwhile, the signals), Swift's newAdjustmentLayersStartFromThePaletteRenderAndEditInThePanel in ImageAdjustmentTests, the request as a gate in SessionGateTests; tests that add an adjustment and edit on clear the request, as Swift's add does.
  • Editing adjustment layers in the window (9.5d): LayersPanel twins Swift's .task(id: adjustmentEditingID): on every changed() (and when made, as a task runs on appear) it compares the request with the one it last began (m_adjustmentTask) and begins a new one; a render already under way for the same request lands nothing, so nothing needs cancelling. The footer's adjustment button is a QToolButton whose menu (Swift's Menu, InstantPopup) lists the six kinds by name and calls addAdjustment, resting while canEditLayers does not hold. Its glyph and the rows' are LayerIcons paths for Swift's SF Symbols; symbol names each kind's (circle.lefthalf.filled, slider.horizontal.3, the curve path turned a quarter clockwise as Swift turns it, plusminus.circle, paintpalette, circle.grid.3x3), and adjustmentThumbnail draws it 1.2 times the rail's size, centred in the row's 36-point square, as the text glyph is. An adjustment's row shows that glyph, "Adjustment · Double-click to edit", and no link for its mask (Swift's linkable); a double click on a control makes it active and asks for its editor (Swift's renameClickedLayer), one on the name renames. The Layer menu opens with Swift's New Adjustment Layer submenu (the kinds with an ellipsis, named new…Adjustment, resting while canEditLayers does not hold) and Edit Adjustment… (the active layer's request, for an adjustment layer alone), then a separator. The Levels sheet's caption says "Underlying pixels · alpha-weighted histogram" for an adjustment's editor, and the filter and Hue/Saturation sheets hide "Limited to the selection" there, as Swift's adjustmentOriginal terms. Tests: Swift's adjustmentEditorsUseMovableNonmodalPanels for every kind in FloatingPanelTests; AdjustmentPanelTests (the footer and its request, the rows and their glyphs, the Layer menu, the sheets' words); the Layer menu's count and names in CompositorMenusTests.
  • Layer effects in the document and the session (9.6a): Document/LayerEffects.h/.cpp twin Swift's model: the four effects with Swift's defaults, LayerEffectKind by raw value in Swift's order (allLayerEffectKinds), LayerEffectSelection, LayerEffects with its helpers. Swift's file runs to 544 lines, so its session extension is LayerEffects+Session.cpp with LayerEffects+Session.h, and its renderer arrives with 9.6b. isValid keeps Swift's bounds and leaves out the isFinite terms the bounds already refuse; offset falls away from the light, y down. The manifest's effects is Swift's synthesized Codable, written by hand in IO/ProjectStore+Effects.cpp: every key required but enabled, which stays out while absent; the store holds no rule for effects, as Swift's. Swift 1.1 gave the record an effects key but its projectSnapshot and installProject never carried it, so its saves and exports dropped the effects; Swift agrees with the twin since 1.2.0 (5775dd1, 6230e41). The twin's record (hierarchyRecord) carries them and ImageLayer(record, …) loads them, so they save, load and, with 9.6b, export. Elsewhere they travel as Swift lists them: equality, a duplicate and the brush's two commits keep them; Invert's, Levels' and Hue/Saturation's commits, a floating merge and both resizers leave them out (the filter's keeps them since 12.9a); a copy between projects keeps them since 12.10b, as Swift 1.2.5's list does. The session's effectsEditing, effectsEditingOriginal and effectSelection are getters; 9.6d gave the first two setters, which notify on a change, for ContentView's check, and made dropEffectSelection public for the canvas's press (the list's arrive with 9.6e). selectLayer, selectLayers and selectLayerTarget drop the chosen effect before any guard, as Swift's, so a refusal still drops it and says so (dropEffectSelection); Delete and the trash take a chosen effect first. setEffects finishes an opacity drag and makes one step of a valid change; changeEffects stays with the panel's layer, a step per change as Swift's; Cancel puts back the panel's effect alone. The colour picker gains Swift's effect target (openEffectColorPicker, previewEffectColor, closeColorPicker's effect case, titles such as "Color Picker (Stroke Color)"); ColorPalette.h declares LayerEffectKind opaquely, since LayerEffects.h includes it. Left out as dead: the isGroup terms of canEditEffects, setEffects and canCopyEffect (a folder never holds pixels, LayerHierarchy::validate sees to it, so asset says the same), selectEffect's selectedLayerIDs = [id] (setActiveLayerID sets it, and selectLayer cannot refuse behind canEditLayers) and its effect picker close (every path that clears effectsEditing closes that picker first, ContentView's check since 9.6d included). Swift has no effects tests: LayerEffectsTests and LayerEffectsSessionTests pin its code, and ProjectCopyTests the copy.
  • Layer effects drawn (9.6b): Rendering/LayerEffectsKernel twins MetalLayerEffects as a namespace (the class only holds its device): the shader's passes in float and in its order, bands of 32 lines side by side: the alpha, the spread rows and columns, the ring, the bilinear shift, the blur rows and columns, the inner shadow's coverage and the composition, each byte rounded as the shader's uchar(x * 255 + 0.5). The spread is Swift's CPU extreme, a sliding window each index enters and leaves once, whose maxima and minima equal the shader's loops, nothing read past the edges; the blur's weights are computed once, since every pixel of the shader computes the same ones, and each tap is added in the shader's order, the columns tap by tap over whole rows. LayerEffectsKernelTests replays the shader's loops as written and compares byte for byte. Metal's 80-million-pixel ceiling guards the GPU's buffers, past which Swift falls back to Core Image; the kernel takes what render allows, one surface (200 million since 12.11a). Its buffers are allocated before any worker runs, through one helper, and one that cannot be is ExportError(render); the spread's queues are sized by each pass's bands and line length, so a thin layer asks little. Workers write the result through one pointer taken before they start: scanLine() detaches, which races. Swift's CPU renderer (shadowCoverage, innerCoverage, ringCoverage, coverage, strokeCoverage, fill) serves machines without Metal and is dead here, as the brush's dab fallback is (8.1). LayerEffectsRenderer is Document/LayerEffects+Renderer.h/.cpp, since Swift's file runs past 500 lines: cached keeps Swift's cache (the pixels and mask by identity, QImage::cacheKey for ===, the switched-on effects by value, eight at most within 64 MiB of results and their sources, first in first out) under a mutex, since the export's worker and the UI both draw; a failure logs on omaphoto.rendering where Swift's try? is silent, and the layer then draws bare. placed, margin and render follow Swift's; the mask is multiplied in with DestinationIn, smoothly scaled, where CoreGraphics clips to it, and the pixels are copied into the padded grid before the kernel runs. ImageExporter::render and drawLiveComposite draw a layer's effects image through placed with its opacity, blend mode and folder clip, the mask already inside it, as Swift's do; the canvas draws them with 9.6c. Equivalent mutant: cached's validity term (render refuses the same effects and the cache's catch returns none; the term only spares the warning).
  • Layer effects on the canvas (9.6c): Rendering/EffectsPreviewCache twins Swift's class, a QObject the session holds as effectsPreviews, Swift's @ObservationIgnored member. Swift's serial queue is a QThreadPool of one thread; a request waits 60 ms on a single-shot timer (precise below two seconds) and then joins the pool; Swift's locked cancelled is a shared atomic flag; the result is posted back and lands only while its request is still the layer's, Swift's id check. Request holds the asset, so a painted layer flattens on the worker. matches, the kept previous, seed, rendered, prepare and the shared budget (64 MiB of output, 32 to 1536 a side) follow Swift's (the recent results and renderNow since 12.12a). A reduced preview scales each source through QImage::scaled's smooth filter, which measures each axis on its own (an area average where it shrinks, bilinear where it grows), for CoreGraphics' high-quality reduction, since a mask may differ in size and shape from its layer; a mask travels as alpha, so its values pass exactly; a render that fails logs on omaphoto.rendering and lands none, as Swift's try?. The destructor drops the queued renders; the pool's own destructor waits for the one under way. The canvas's completion repaints the view through a QPointer, Swift's [weak self]. A fix beyond Swift: Swift scales the stroke and the drop shadow for a reduced preview but not the inner shadow, which then shows too wide on any layer past 1536 pixels; the twin scales its distance and blur too.
  • Rendering/LayerEffectsSurface twins Swift's class: make for its failable init (80 million pixels at most, a warning past it or on a failed allocation), matches, update (tiles taken by origin and cacheKey, Swift's ObjectIdentifier), compose and window. Swift's CPU fallback (fill and the renderer's coverages) serves machines without Metal and is dead here, as the renderer's is; a pass paints on a copy of the surface and publishes it only once whole, so a window, kernel or copy that cannot be allocated logs and leaves the last image, and a pass's tiles count as taken only once redone, so the next update tries them again (a fix beyond Swift, which marks them first and after a failed first pass would show the empty context). The window draws a painted base from its tiles, the tiles over it, then multiplies the mask into the layer's own pixels once (DestinationIn, smoothly scaled); Swift draws the base and the tiles through the soft clip each, so a half-covered pixel keeps a quarter of the old pixels under the paint (known difference). Fixes beyond Swift: Swift widens a change by the margin, the room outside the layer, and reads a reach round that which leaves out the inner shadow, so an inside stroke or an inner shadow stays stale past the margin and the inner shadow is cut at the window's edge (dark bands while painting). The twin widens and reads by the reach, the inner shadow's included, and stops every window at the surface's bounds, where the renderer's padding ends: a first pass equals LayerEffectsRenderer::render byte for byte, and every later pass a fresh render of the painted pixels (LayerEffectsSurfaceTests). Left out as dead: the reach's isEnabled terms (the surface is made from the shown effects), Swift's empty-region guard and its zero-size terms (a stroke's grid is a pixel at least, and a change lies inside it), and the reach's ceil and the region's integral (every box is made whole where it is used, and tiles lie on whole pixels).
  • The canvas's effects (9.6c): DisplayState::Layer carries effects, drawLayers calls prepare, and synchronizeDisplay hands a finished stroke's surface to the cache as a seed first (handOnStrokeSurface, Swift's opening lines). drawOwn keeps Swift's order: the warp, the distorted effects, the distortion's preview, the effects preview, then the stroke, the shape's preview and the colour edits' previews, so a layer with effects shows its effects over its own pixels while Levels, Hue/Saturation or a filter previews it, as Swift's does (EffectsCanvasTests pins it). A fix beyond Swift: Swift asks the cache twice a draw, first with the mask unplaced and the stored transform, then with what the layer shows; the first request wins, so a placed mask clips the preview unplaced, and while a transform edit moves such a layer the two requests cancel each other and render without end. The twin asks once, with the shown mask and transform, before the distortion's branches, so the mask is worked out first. The drawing functions lost their const: they update the stroke surface and hand the cache a repaint. drawStroke moved to EditorCanvas+Effects.cpp beside strokeSurface and handOnStrokeSurface, keeping EditorCanvas.cpp under 500 lines. Distort.cpp gains DistortEffectsCache, both distortedEffects (const with a mutable cache, as distortPreview; a warp that throws leaves the effects off, Swift's try?) and commitDistort's seed, the cache cleared after the step as Swift's defer clears it. Swift has no tests for any of this: EffectsPreviewCacheTests, LayerEffectsSurfaceTests and EffectsCanvasTests pin its code. Equivalent mutants: the pool's thread count (a result lands by its request's id, whatever the order), the destructor's clear and every cancelling of a superseded request, the worker's two checks included (the landing's id check drops them; they only spare renders), the matching's && between two absent masks (the sources must match anyway), !previous before the seed (a landed result clears its seed), the resizes skipped at full size (a draw at 1:1 copies); in the surface, the reach's floor of one (an overlay reads each pixel alone) and the stroke's + 2 (the spread reads lround(size)), inner's cut at the bounds (QPainter clips the draw alike), the first region's margin (the reach goes as far) and the window's skip of tiles off its region (it only spares draws); commitDistort's clearing of its cache (keyed by its inputs, the cache only frees memory). EffectsFailureTests starves a stroke's first pass through AddressSpaceLimit, in a binary of its own: the canvas logs it and shows the last effects. Untested failure paths: rendered's and the landing's tests of a result, and the resize's null check, differ only when a preview fails, and a preview asks too little for AddressSpaceLimit to reach a worker's arena.
  • Layer effects' panel (9.6d): UI/EffectsSheet.h/.cpp twin Swift's sheet: 340 points wide with 20 of padding and 16 between rows, the kind's headline (13 pixels, semibold), the stroke's Position as two touching checkable QToolButtons in an exclusive QButtonGroup (Swift's segmented picker; its hidden label is the box's accessible name), the colour as the Type bar's SwatchButton (36 by 18, radius 3, a half-black rim, <Kind> color its tooltip and accessible name, black while the effect is missing) opening openEffectColorPicker, Swift's slider rows (a 64-point title as the slider's buddy, a 130-point slider over a thousand steps of its range that sets the value unrounded, as Swift's continuous binding, a 48-point right-aligned PickerField with the title as its accessible name and placeholder, its unit two points after), then Cancel and OK ten points apart, OK the default button and Cancel no auto-default, so Return anywhere means OK, as Swift's .defaultAction. A field shows its value whole, ties to even as Swift's .number format rounds; it applies a readable finite number on leaving or on Return (which then goes on to OK), clamped to the slider's range as Swift's binding, reverts other text, keeps its typing while other changes arrive, and steps with Up and Down (Shift ten) from the session's value, clamped, the step replacing any typing and showing, as the picker's entries do; a change through the row's own slider (a wheel leaves the keys with the field) replaces it too. Each change is a step, as Swift's changeEffects. On every change of the picker's colour the sheet calls previewEffectColor (Swift's onChange), and it repaints its swatch on every change. While the layer lacks the effect the controls hide, as Swift's if let effect. ContentView::synchronizePanels first runs Swift's layers check (an edited effect gone from its layer, or the layer gone, closes an effect picker without its colour and clears effectsEditing and effectsEditingOriginal; a palette's picker stays), then shows the sheet in effectsPanel, titled by the kind, whenever effectsEditing changes (a new choice, a new sheet), its close button and Escape cancelling, and closes it when the choice clears. The footer's effects button (sparkles in LayerIcons: three four-pointed stars) is an instant-popup menu of the four kinds, each with an ellipsis, resting while canEditEffects does not hold. While an effect is chosen (selectedEffect, so a choice an undo took away names the layer again) the trash says Delete selected effect and the Layer menu's Delete Delete <Kind>. The canvas's left press lets go of a chosen effect before any guard, Swift's first line of mouseDown; other buttons keep it. Swift has no tests for the window's effects: EffectsPanelTests pins them, and LayerEffectsSessionTests the new writers' signals. Equivalent mutants: the fields' OmitGroupSeparator (no range reaches a thousand) and OK's explicit setDefault (a dialog makes its first auto-default button the default as it shows, and Cancel is none).
  • Effects in the list (9.6e): LayerEffectRow (declared in NativeLayerList.h, its code in NativeLayerList+Effects.cpp) twins Swift's private class: a 24-point row under its layer for each effect, in Swift's order, the cell 52 points tall and 24 more for each (rowHeight, effectHeight). The list activates its column's layout after configuring the cells, as noteHeightOfRows changes heights at once, so no press lands on a row that a taller one still covers. The eye (20 by 22, 38 points in plus the layer's step, flat, eye or eye.slash in PlaceholderText, named Hide <Kind> or Show <Kind>) toggles the effect in one step and rests while canEditLayers does not hold; the name (11 pixels, WindowText, PlaceholderText while hidden) ends in an ellipsis when narrow, as .byTruncatingTail; the row's tooltip is Swift's with Alt for Option, its accessible name <Kind> effect, and an accessible interface of Swift's group role chooses the effect on the press action (accessibilityPerformPress). The rows are made anew only when the kinds change, else reconfigured in place; the old ones are hidden and deleted later, because a row's own double click may cancel a new effect's panel on the same layer, whose Cancel removes that effect and rebuilds the rows under the press, where Swift's reference counting keeps the row alive. A chosen effect's row shows Highlight at 30%, and while one is chosen the cell's selection hides, as Swift's table selection does. Presses follow Swift's mouseDown order: Alt without Ctrl on the strip (the bottom 10 points of the taller row, over its last effect; Swift's min(10, height / 3) is dead, rows being at least 52) clips through clickRow; on an effect, Alt without Ctrl arms a copy while layers can be edited, the release choosing the effect and a move past startDragDistance with Alt still held starting a QDrag of UUID:Kind under effectType with the list's token, offering Copy alone under the duplicate cursor, as the row's Alt drag does; any other left press chooses the effect, a double click editing it (Swift's clickCount > 1); the eye's Alt presses are the row's, ignored in its filter so they go on to it (Swift's hitTest). An effect dropped from the same list (Swift compares the source's session) lands on the row under the pointer, filled, as a copy, where canCopyEffect allows. A left press below every row lets go of the chosen effect and, without Ctrl or Shift, of every layer, as Swift's table with allowsEmptySelection does (a gap the list of 4.6 left). Known difference: the two points between rows take no press, where Swift's rows take their spacing. Swift has no tests for the list's effects: LayerEffectRowTests pins them, with QDrag::exec defined in its binary as in LayerDragGestureTests.
  • Remove Background (9.7): Apple's Vision has no Linux twin, so Document/SubjectRemoval runs U²-Net (Qin et al., Apache-2.0, its text in licenses/U-2-Net.txt) on ONNX Runtime (MIT), as rembg converted it (MIT): u2net.onnx, 175,997,641 bytes, SHA-256 8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491, the MD5 rembg pins. Chosen over u2netp (poorer edges), silueta (no clear source) and IS-Net's isnet-general-use (sharper, but the DIS repository puts only its code and metric under Apache-2.0 and its DIS5K training set under terms of its own, so the weights' terms are unclear); 90 ms a mask on 32 cores, measured. The model is found with QStandardPaths::locate(GenericDataLocation, "omaphoto/u2net.onnx"), so a package installs it as share/omaphoto/u2net.onnx and the dev image carries it in /usr/local/share. Dockerfile.dev fetches it and Microsoft's ONNX Runtime 1.21.0 build (Ubuntu 24.04 packages none; Ubuntu 25.04 on and Arch ship theirs with the same CMake config), both by pinned checksum; that build's CMake config goes to /usr/local/lib/cmake, which Ubuntu's CMake searches, and its library to lib64, where the config looks for it; CMake links onnxruntime::onnxruntime. One Ort::Session serves the process, loaded on first use; a load that throws leaves it unloaded, so the next use tries again. Every model step passes through vision, where ONNX Runtime's exceptions become SubjectRemovalError(model) and a failed allocation (the input buffer, the runtime wrapper's own vectors) becomes ExportError(render), so none passes a worker's catch. SubjectRemovalError twins SubjectRemoval.Failure: noSubject with Swift's words, and a model kind for a missing model (naming every folder looked in) or ONNX Runtime's own message, where Swift passes on Vision's error. The input is the paper's and rembg's: the image premultiplied (clear reads as black), scaled to 320 by 320, divided by its brightest byte, then ImageNet's mean and spread; the first output is stretched from its lowest to its highest value, truncated to bytes as rembg does, and scaled to the layer's grid. An output with no value at or above one half is Swift's noSubject (flat images give under 0.03, measured). Known difference: U²-Net's mask is made at 320 points, so Basic's edges are softer than Vision's; Advanced's Refine pulls them onto the layer's own. The raw mask of the last image is kept by cacheKey, Swift's MaskCache. Document/GuidedMatte twins Swift's file: box keeps Swift's float sums, the column pass running a row at a time; filter frees what it no longer needs; levels scales through premultiplied RGBA (Qt's smooth scaling of Grayscale8 strays by a level shrinking and up to ten enlarging, probed) and reads gray over black through qGray; refine draws back up with SmoothTransformation, and the model's mask is enlarged through premultiplied RGBA too. refined does Core Image's Shift Edge (a Gaussian with the edge repeated through PixelAdjust::gaussianBlur, then Swift's clamp-and-scale cut) and Contrast in float over bytes, rounded once; the blur rounds to bytes first, a level at most. Contrast runs unconditionally, being the identity at 0. The preview (run) refines a copy 1400 long at most and multiplies each premultiplied byte by the mask into pixels of its own, copied before the loop so a copy that finds no memory is ExportError(render) (a shared image would copy inside the loop and come back null); the commit (subjectMask) refines at full size, multiplies in a mask of the layer's own grid (another grid is a logic_error, never read past) (disabled or not, which is then shown, as Swift's), blends through a selection over that mask or over white, and writes the mask in one Remove Background step with the mask chosen; its worker flattens painted pixels and masks. A fix beyond Swift: a placed mask's pixels were dropped but its placement kept, so the new mask, made in the layer's grid, sat at the old placement; the twin clears the placement. The filter sheet shows Swift's words, Quality as two touching checkable buttons (the label hidden, its accessible name) and Advanced's Refine, Contrast and Shift Edge rows with Swift's help, made once and shown while Advanced is chosen, as Swift's if. Equivalent mutants: the cache's key (only the time differs) and the commit worker's catch of SubjectRemovalError (after its preview succeeded, only ONNX Runtime failing mid-run could throw there). Tests: GuidedMatteTests (the box against plain sums, the filter against He's steps in doubles, overshoot included), SubjectRemovalTests (Swift's visionRequestRunsOnAnImage among them, on a red disc the model finds), SubjectRemovalModelTests (a binary of its own: a broken model, a missing one, then the real one), SubjectRemovalFailureTests (a starved preview and a starved model input, under AddressSpaceLimit, glibc's mmap threshold fixed so large requests map memory the limit counts), RemoveBackgroundTests, the sheet in FilterControlsTests, the kind in FilterModelTests and both menu tests.
  • Crop (10.1): Document/Crop.h/.cpp twin Swift's file, Crop+Session.h its session half. CropGeometry is a namespace; CropDrag::Mode is a kind and an index, as TransformDrag::Mode twins an enum with payloads; CropSnap's nearest and Swift's local shift are file helpers; cropSnapTargets returns SnapGuides, Swift's tuple. valid and commitCrop read the frame normalised, as CGRect's width and minX read it standardised; valid's isFinite terms are left out, since the ranges refuse NaN and infinities. cropRect, cropRatioChoice and cropError are getters and setters that notify on a change. commitCrop takes a completion callback, posted on every path; the snapshot is taken and the worker started before anything changes, the resize runs through QtConcurrent::run and a QFutureWatcher with the project busy, the worker catching ProjectError and ExportError (CanvasResizer::resize turns a std::bad_alloc of its records and maps into ExportError(render), as 9.7's vision does), and the result is moved out (takeResult) and installed through applyDocumentSize, one Crop step; memory that runs out while the snapshot is taken or the result lands sets cropError to the render error and changes nothing else, so the project is never left busy. Only a crop that landed drops its frame (Swift drops it first); a failure sets cropError to the error's text and keeps it, and the project is freed after either, Swift's defer. Hooks: selectTool cancels on a change of tool and opens the Crop tool on the whole canvas, Free, when no frame is drawn; restore, beginTransform, importImages, beginLevels, beginFilter and ProjectController::begin cancel; installProject and clearProject drop the frame and announce it with their own signal; canEditLayers reads cropRect, canUseHistory does not, as in Swift. The canvas's gestures live in EditorCanvas+Crop.cpp. A press on a grip resizes (the first region holding the point, corners first, Swift's first(where:)), one inside a drawn frame that is not the whole canvas moves it, any other drops the frame and draws anew; CGRect.contains leaves out the far edges, so the file's own contains does too, where QRectF::contains takes them. The snap is built at the press, 8 view points (cropSnapDistance); Alt keeps the middle still, Ctrl leaves the edges unsnapped (Swift's Control, as the Move tool's snapping reads it), and either key changing mid-drag reshapes the frame at the last hover point, as Swift's monitor reads the mouse. Swift's ?? CGRect(origin: pixel, size: .zero) and its if let cropSnap are dead (the Crop tool always shows a frame; the snap is built with the drag) and are checked reads. A drag goes on under the left button alone, with the Crop tool, a free project and a document, as Swift's mouseDragged; a move without the left button ends it, since Qt can lose a release. endCropDrag is Swift's cropDrag = nil with its didSet: the release, a lost focus, Escape (cancel) and Return or Enter (commit) end the drag and free its cursor. Wheels and pinches rest mid-drag. The drag keeps the cursor its press showed in m_dragCursor (Swift's .current, with the cursor rects disabled), taken before a new frame drops the old one. Cursors: picking first, then the open hand under Space, then over a grip Qt's resize shape for Swift's frame position (top left and bottom right SizeFDiagCursor, top right and bottom left SizeBDiagCursor, top and bottom SizeVerCursor, left and right SizeHorCursor; Qt's shapes say no inward or outward), elsewhere the crosshair. TransformOverlay gains cropViewRect, cropResizeRegions (a CropRegion struct for Swift's tuple) and drawCrop: the canvas outside the frame under 60% black (a QPainterPath's own odd-even rule), a white frame, the thirds at 40% white in one path so their crossings paint once, and 8-point white grips with black rims, antialiased, with CoreGraphics' butt caps and miter joins. Known difference: at 1× QPainter strokes one-point lines through its cosmetic stroker, which draws no joins, so the outer corner pixels of the frame and the grips stay partly clear, as the text box's (8.9c). The dimming spans the canvas, so a frame that appears or goes repaints the canvas; a frame that changes repaints the old and the new frame with their grips (drawnRect), since outside both the dimming stays. That is the twin's cropDraggingRedrawsOnlyTheOverlay: Qt has no layers, so the union of the frames repaints, pixels included, never the canvas. Under the Crop tool renderBounds takes the frame's union, so a frame past the canvas shows the checkerboard it would take in and redraws the canvas, as in Swift. CropControls is the tool's ToolHeaderBar: its row spaced 14 as Swift's HStack, Swift's labelled picker a "Ratio" QLabel (its combo box's buddy) and a QComboBox sharing 170 points, a choice that changes the ratio calling changeCropRatio (Swift's onChange), the frame's size as whole pixels (Swift's Int, truncated) grouped through the English locale like the status bar (monospacedDigit has no Qt 6.4 twin), and Cancel and Apply Crop resting without a frame; the bar rests while busy shows or without a document. ContentView shows it for the Crop tool and the "Couldn’t crop" alert over cropError. Left out as dead: renderBounds' tool test (a frame lives under the Crop tool alone: every other way to another tool cancels it or asks canEditLayers, which a frame refuses) and the strips' max(0, …) in cropResizeRegions (a strip it would clip lies under the corners' regions, which take the point first). Equivalent mutants: contains on the visible frame for the drawn one (while a frame is drawn they are one), the tolerance's max(…, 0.0001) (the zoom's floor keeps points per pixel above it), cropMove's answer after a lost release and after a step (the rest of the move only refreshes the cursor), the strips' ends (under the corners), the frame's caps and joins (its corners lie under the grips) drawCrop's setBrush(Qt::NoBrush) (the painter arrives with none) and takeResult for result() in the landing (a copy asks only more memory, and its failure is caught the same). Tests: CropTests (Swift's six session and geometry tests; symmetric drags, snapping by side, bounds, ratios), CropSessionTests (a failed crop, refusals and the busy project, the signals, every hook, a flipped frame, one step that ends redo), CropFixtures.h (committed), CropCanvasTests (Swift's cropDraggingRedrawsOnlyTheOverlay; drags, Alt and Ctrl mid-drag, keys and lost releases, cursors, the dimming, a frame past the canvas), CropControlsTests, CropFailureTests (a binary of its own whose main shares the main heap among threads, M_ARENA_MAX 1 before any thread starts, and maps large asks, so a worker's allocations meet AddressSpaceLimit: a worker in an arena of its own grows its reserved heap past the limit; the snapshot, the worker and the landing each run out in turn), the bar and the alert in ContentViewTests.
  • Canvas and image size (10.2): flipCanvas sits in LayerFlip.cpp, as Swift's extension: after commitTransform and cancelCrop it asks canEditLayers, mirrors every layer's transform and every placed mask across the canvas's middle and the selection's outline through a QTransform (the path keeps its fill rule), one Flip Canvas Horizontal or Flip Canvas Vertical step; Swift's finishOpacityEdit is left out, since commitTransform has run it. UI/CanvasSizeSheet is Swift's form in a 450-point column: the title at 17 pixels, bold, the current size grouped through the English locale, the memory as Foundation's ByteCountFormatter counts it for memory (binary units: bytes, whole KB, MB to one place, GB to two, trailing zeros dropped; UI/ByteCounts since 10.3), a Mid divider, Units as a labelled QComboBox, Width and Height as PickerFields (a readable number applies on Return or on leaving, other text goes back, Swift's value binding; printed as Swift's .number to three places, none trailing, never grouped, as every field of the twin, halves rounding to even as Foundation's formatter rounds them), the Relative and Lock toggles (locking takes the width's proportions at once, Swift's onChange), the New line or the invalid note in BrightText, the anchor grid (nine 25-point buttons in an exclusive QButtonGroup, each a circle filled in the accent when chosen for Swift's circle and circle.fill, with Swift's help as the tooltip and accessible name and "Selected" as the accessible description, Swift's accessibility value), the anchor's name and note, the Canvas extension picker, and for Custom Swift's ColorPicker as a label and a 36 by 18 SwatchButton that opens a QColorDialog without alpha, the desktop's colour dialog where SwiftUI opens the system panel; then Cancel and the default OK, which rests while the draft is invalid, so Swift's guard in its action is left out. Units, Relative and Lock change only through their own controls, so nothing writes them back, and so does the image sheet's Resample. A wheel over Units changes the unit without the focus, so both sheets take the focus to Units first: a size still being typed commits in the unit it meant. UI/ImageSizeSheet is Swift's sheet: Pixels and Percent only while resampling, Width and Height converting by unit (Swift's binding refuses a number that is not positive; without resampling a size sets the resolution), Lock, disabled without resampling, the resolution with its unit (in inches or centimetres a new resolution scales the pixels, Swift's onChange), Resample (turned off, the pixels go back, the lock closes and Pixels or Percent becomes Inches), Sampling while resampling, the explanation, the result or the invalid note, Cancel and the default Resize. The controller's canvasSize and imageSize live in IO/ProjectController+Resize.cpp (with 10.3's exports the controller's file would pass 500 lines) and take a completion callback: refused without a window, a document or begin, as Swift's guards; the sheet is a window-modal QDialog over the window (sheet, Swift's beginSheet), whose Escape and close button cancel; the project stays busy until the sheet answers and the resize lands, or its alert is dismissed, Swift's defer. resizeProject takes the snapshot and starts the worker before anything changes, the worker catching std::runtime_error (both of the resizers' errors), and moves the result out and lands it through applyDocumentSize or applyImageSize (staged since 10.1); memory that runs out while the snapshot is taken or the result lands is explained as the render error. The alert opens from the handler, once the unwinding has freed what failed. The IO file includes the sheets, as Swift's controller hosts them. The Image menu gains Canvas Size… (Ctrl+Alt+C) and Image Size… (Ctrl+Alt+I), enabled with a document while the controller can start, and Flip Canvas Horizontal and Vertical, enabled by canEditLayers. Tests: CanvasSizeSheetTests, ImageSizeSheetTests, ProjectResizeTests (the sheets over a window, typed and answered by keys, a cancel, a refusal, a failure's alert), ProjectResizeFailureTests (a binary of its own, main as CropFailureTests': the image resizer, the snapshot, the worker and the landing each run out), the canvas flips in LayerFlipTests, the entries in CompositorMenusTests.
  • 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 preview on gray 0.12 (the encoded preview fitted, centred and smooth; 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 36 by 18 SwatchButton that opens a QColorDialog without alpha (the same colour asks nothing, as Swift's onChange), the raster's size grouped with "px · sRGB", then the error in BrightText, or the file's size and "· encoded preview, fitted to window", or "Updating preview…", 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 panel limits sit at 2,560 KB, above the snapshot's peak of 2,300 KB (768 KB and a 1,532 KB mapping, reached before the panel allocates) and one arena growth, below a copying mutant's 3,060 KB peak, measured in 12.8d, because the heap's free chunks count on top of a limit and shift with load; limited's malloc_trim(0) is an equivalent mutant, since no free top chunk serves a 1,532 KB mapping), 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-<UUID> 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.
  • 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 namespace), 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.
  • Rulers, and guides on the canvas (12.3b, Swift 1.1.7): UI/CanvasRulers.h/.cpp twin Swift's file: CanvasRuler::thickness, CanvasRulerCorner and CanvasRulerView (a widget per strip, repainting on every changed(), where Swift's canvas asks each ruler to redraw), with Swift's majorStep and label. The canvas is the rulers' sibling, so a ruler maps its points through the screen (inCanvas), where Swift reads its own bounds as the canvas's. A press hands the canvas the keys and begins a guide from that ruler (horizontal from the top one), a drag moves it, a release over a ruler drops it; a ruler has no mouse tracking, so it sees no buttonless move and leaves a lost release to the canvas's next press. ContentView frames the canvas slot in a grid with the corner and the two strips, shown while showsRulers holds and a document is open. TransformOverlay draws the layout grid (majors solid gray 0.7 at 45%, subdivisions dotted one point on and two off in gray 0.55 at 28% once four points apart, both a hairline) and the guides (EditorSession::guideColor, a hairline across the whole view) first, as Swift's draw; the canvas redraws the view when the grid's flag, the guides' flag or the displayed guides change (GuidesShown), Swift's observation. Guide drags live in EditorCanvas+Guides.cpp: with the Move tool a press on a guide (canEditGuides and hitGuide) moves it ahead of any layer under the resize cursor of its axis, a drag follows, a release over a ruler (isOverRuler: a point left of or above the canvas while the rulers show) deletes it; Escape cancels it ahead of a transform edit's keys; wheels and pinches rest while one is dragged; the Move tool's hover shows the guide's resize cursor after the handles and before a layer's move cursor. Qt can lose a release: a buttonless move and a lost focus finish the drag where it is. Equivalent mutant: the ruler press's canEditGuides test (beginGuideCreation asks it again). Tests: GuideCanvasTests (Swift's moveToolDragsAGuideAndRulerDropDeletesIt and rulerStepUsesNicePixelIntervals; the drawing, repaints, cursors, keys, lost releases, the rulers' layout, ticks, repaints, focus and drags), the canvas's place in ContentViewTests.
  • The Magic tool's Object mode (12.4, Swift 1.1.8): Document/ObjectSelection.h/.cpp twin Swift's file, ObjectSelection+Session.h its session half; WandMode (Swift's raw values, allWandModes) sits in Selection.h. Vision's foreground instances have no Linux twin (the user's decision): SubjectRemoval::foreground hands back U²-Net's mask at the image's size (the Remove Background model and cache, 9.7), which select scales to the model's 320 grid, cuts at half, and splits into 8-connected regions (region, the neighbours a traced outline joins); the region under the clicked cell is the instance, background none, and a picture where the model finds no subject none, as Vision's empty result. Its cells are enlarged smoothly to the image and refined by GuidedMatte::refine (radius 5) on the image, for Swift's edge-preserving upsample, then cut at half; adjusted erodes or dilates a pixel ring a step (eight neighbours, ten steps at most, the image's edge counting as inside, Swift's eroded and dilated), MagicWand::outline traces it and smoothed rounds the stairs as Swift's (the start Qt's close repeats dropped first, since a CGPath's close adds no point, then a subpath under three points dropped, Douglas–Peucker at 1.6 from the leftmost point, Chaikin three times, the fill rule kept; Swift's later drops of a repeated end and Chaikin's guard under three points are dead after that and left out). A bad_alloc is ExportError(render); Swift's unsupported has no twin. The worker catches SubjectRemovalError, MagicWandError (the trace's refusals) and ExportError, each landing in brushError as Swift's catch. selectionSample is the wand's sample made public as Swift's; selectObject runs through the wand's watcher and landing (Wanding carries the step's name, Object Selection), with a completion callback, the offset clamped to ten either way and the anti-alias setting as its smoothing. pressWandKey chooses the tool, W on the canvas and in the list, a repeat ignored; Tab steps the mode; the tool's label, its shortcut definition (Magic) and the bar's title say Magic. The bar (LassoControls) shows Wand and Object as checkable buttons for Magic alone, the colour wand's controls in Wand mode, and in Object mode This Layer and All Layers and an Edge field of −10 to 10 px (SelectionAmountField takes a sign when its range goes below zero; a lone minus changes nothing, an overflow reads as its end), with Swift's help; the anti-alias help follows the mode. The canvas's press runs selectObject ahead of the colour wand, a click inside the selection picks afresh, the cursor is the crosshair with SelectionIcon::objectSelection (haloed by eight offsets as every selection icon, where Swift strokes it white), and the rail draws the icon for Object mode (ToolIconKind::object); the status hint follows the mode. Known difference: U²-Net finds one salient mask, so objects touching in it are one region where Vision may tell instances apart. Left out as dead: the mode picker's cancelLasso (Magic never holds a draft: beginLasso refuses it and selectTool cancels one), selectObject's busy term (canEditSelection reads it) and its clamp of the offset (adjusted clamps), and a null check of the enlarged cells (GuidedMatte::refine checks its own). Equivalent mutants: the grid's cut at exactly 128, simplifyClosed's guard under four points (the fallback returns the same points), finish's three-point threshold at four (a traced loop has four corners), the Chaikin reserve, the edge's rounding (typing and arrows give whole numbers), W calling selectTool (pressWandKey is that call). Untested failure paths: the region's own 320-cell image, 100 KB, which no limit starves without starving the grid before it, and the worker's MagicWandError: a refined mask never nears eight million edges, and the trace's image-sized buffer asks less than the guided filter before it. Tests: ObjectSelectionTests (regions, edges, smoothing, the model on red discs, the session, keys, the canvas), the bar in LassoControlsTests, the icon sheet, the rail and hints in ContentViewTests and ContentViewStatusTests.
  • Photoshop import (12.5a, Swift 1.2.0): src/IO/PSD/ twins Swift's IO/PSD folder. PSDTypes holds PSDError (Swift's five cases and words), PSDConversion, PSDRecord, PSDDocument and fromPSD (Swift's LayerBlendMode.fromPSD); Swift's PSDLayerKind.other is never assigned, so the case and its note are left out. PSDChannelCoder decodes raw and PackBits planes (a no-op run skipped, every overrun truncated) and builds RGBA8888_Premultiplied pixels tagged sRGB, rounding as Swift's (c × a + 127) / 255, and Grayscale8 masks; a surface that cannot be allocated is ExportError(render) through BrushRaster::context, where Swift says truncated. PSDReader reads Swift's fields in Swift's order over a Cursor whose bounds check cannot overflow (an 8B64 length near qint64's top reads as truncated, where Swift's offset + count traps), maps the file where it can and reads it where it cannot (a pipe, a mount without mmap), as mappedIfSafe does (a file that cannot be opened or read is unreadable, where Swift passes on the Cocoa error), logs every refusal on omaphoto.io and rethrows it. Pascal names are Mac OS Roman through a table of the 128 high code points taken from Python's mac_roman (Qt decodes it only through ICU); keys are ASCII or nothing, as Swift's .ascii. PSDAdjustments::parse (in PSDReader+Adjustments.cpp) reads Levels, Curves and Hue/Saturation as Swift does. PSDVector draws vector masks with QPainterPath (tight boundingRect, a path empty only without elements as a CGPath), fills by nonzero winding and strokes with Qt::SvgMiterJoin at 10, SVG's miter rule being CoreGraphics' (probed: kept to a ratio of 9.57, bevelled from 10.43; Qt::MiterJoin measures from the join point and clips instead); integral stays in doubles, so a wild box reaches the size check instead of an overflowing QRect. PSDDocumentBuilder makes the layers and conversions; a shape's identity is its asset's. ImageImporter::loadPhotoshop and photoshopAssets run on the import's worker, makeImport on the UI thread. In the session (EditorSession+Imports.cpp) a file whose first bytes are 8BPS takes the Photoshop path: beginPSDReading shows the sheet while a QFutureWatcher reads it, finishPSDReading fills it or takes it away, finishConversion answers from the event loop (a resumed task), Cancel while reading drops the file silently, a failure joins the import's failures, and a confirmed file lands through insertPhotoshop (a new canvas at the file's size and resolution, or one folder named for the file inside the active folder, centred on a drop point, 10,000 layers at most). A request of Photoshop files alone is one Import Photoshop File step. Fixes beyond Swift: insertPhotoshop validates the hierarchy first, so folders nested past its depth refuse the file (truncated), and a folder clipped to the layer below takes no link but the base note, where Swift links it; either way Swift admits a document its store refuses. Left out as dead: the gates' showsConversionSheet terms and its didSet (the sheet shows only while importing, which the gates refuse already), Swift's test hook confirmConversions, confirmPSDConversions and NativeLayerList.moveLayer (no caller), the request's id, the builder's folder and adjustment checks on a clipping base (only pixel layers become bases), the reader's layerSectionEnd, the default mask colour, the reader's plane-size checks (a plane decodes to its size), resolution's finiteness test and its floor, the vector code's finiteness tests on boxes built from finite numbers and the origination's notes (never set), Curves' two length checks past the first, pixelSize's area in doubles (its boxes are whole, so the whole pixels decide alone), the session's catch around makeImport (every image arrives with its asset, so nothing there allocates), and the new folder's removal from the collapsed set (a fresh id is in no set). UI/PSDConversionSheet is Swift's sheet in a window-modal QDialog (conversionSheet) that ContentView shows from the session's request: the title at 17 pixels bold, the words, a spinner while reading or the list of layer names over their notes, Cancel and the default confirm button, resting while reading; Escape and the close button cancel, and a sheet the session takes away answers nothing; open() makes the dialog window-modal. The request shown is kept, so an unrelated change keeps the content, and replaced content hides at once and is deleted later. The importer's filter and the desktop entry take .psd (image/vnd.adobe.photoshop); a dropped picture is copied as Dropped.<suffix> into a folder of its own, as Swift 1.2.0 keeps a dropped file's name. Equivalent mutants: the cursor's own bounds refusal and skip's (every read past the end meets another refusal: a short signature, a short plane), PackBits' count and row-end checks (the branches refuse the same bytes; they spare reading past the buffer), the reserve calls, painter.end() before the raster returns, the drop copy's close() before its static removal, and the logs. Untested failure path: a read that fails after the file opened (an I/O error; Qt 6.4 refuses to open a directory). Tests: PSDRoundTripTests and PSDVectorTests (Swift's, through tests/PSDFixture.h, Swift's writer, and PSDVectorFixtures.h), PSDReaderTests and PSDReaderLayerTests (the reader's edges, every cut before the last layer byte, budgets and vector layers read whole, built by PSDReaderFixtures.h and PSDVectorBuilders.h), PSDImportTests (the session and the sheet), and the drop, importer, message and desktop checks in their files.
  • 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.
  • 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.
  • The Camera Raw panel's other groups (12.8b2, Swift 1.2.3): UI/CameraRawColorControls.h/.cpp (Curve), +Mixer.cpp and +Grading.cpp, UI/CameraRawDetailOpticsControls and UI/CameraRawGeometryCalibrationControls twin Swift's three files, each view a QWidget over the session, synchronized on changed(). UI/CameraRawRow is Linux-only: the row each Swift view builds for itself (sharpenSlider, opticsSlider, geometrySlider, calibrationSlider, the curve's and mixer's sliders, 12.8b1's slider): a title of at least or exactly a width, CameraRawSlider, a field or none, a double click on the title resetting where Swift's main groups do; each view rounds the slider's value as its Swift twin does (Detail to whole numbers, Optics when its range starts at zero, Geometry and Calibration always, the rest not). Swift's segmented pickers are cameraRawSegments, touching checkable buttons in an exclusive QButtonGroup. The curve graph: a still press and release adds a point, a drag past two points moves the nearest divider or inner point, a double click removes the nearest inner point within 0.04 and its trailing release adds nothing, so the net is Swift's: SwiftUI adds on each click, repair drops the second as a duplicate and the double tap removes one. The graph is CameraRawCurveGraph: a class defined in a .cpp outside an anonymous namespace is global, and the Curves sheet's CurveGraph would share its vtable across the link. The mixer's rows whose help or track follow its tab, swatch or chosen point are made anew when those change, as SwiftUI renders them anew; a picked colour's swatch is shown at once, since a layout shows late children only later. The grading wheel fills a QConicalGradient, which runs counterclockwise from the right as the hue does (1.2.9's 96edd5f), takes presses and double clicks inside its circle alone (Swift's contentShape), and Swift's theDockedPanelFitsTheGradingWheels holds; its stops are 96edd5f's own, pure HSV hues every 30 degrees (v1.2.3's six SwiftUI system colours predate that fix). Detail's idle noise rows are disabled under a 45% QGraphicsOpacityEffect, Swift's .opacity(0.45).disabled. Targeted Adjustment and Draw Guides are checkable push buttons, checked while armed, where Swift tints them (known difference); their SF Symbols, scope and line.diagonal, are LayerIcons paths. The Filter menu lists Camera Raw Filter… after Lens Correction, as Swift's. Known differences: a mixer family's field still being typed in loses its typing when the tab changes (the row is made anew), where SwiftUI's field would commit into the new tab; Equivalent mutants: the wheel's saturation cap (normalized clamps it), the picked swatches made anew only when their count changes (it spares rebuilds; Qt guards a swatch deleted in its own click) and the page menu's size policy (layout). The point-colour eyedropper is flat like the other two, where Swift leaves it bordered (cosmetic). Cosmetic, Swift's values and unpinned: the swatches' 18 points, Detail's idle 45%, the hue ranges' inset (title width and 10), the rows' spacing of 10 and the 11-pixel subheadlines. Tests: CameraRawRowTable.h walked by CameraRawGroupRowsTests (every row against Swift's values), CameraRawGroupDrawTests (drawing, pages, gestures, the panel through FilterSheet), CameraRawGroupWordsTests (every text, captions, buddies, the pickers' states, the mixer's pages, the wheel's lower half and rim, the curve's line, repaint and selected point), CameraRawCurveMixerTests, CameraRawGradingLensTests, the menu's entries in CompositorMenusTests.
  • Hash containers on the render path (12.8e): Qt 6.4 declares QHashPrivate::Data::findOrInsert noexcept while it allocates, so a QHash or QSet insert that finds no memory calls std::terminate, past every catch (found by a backtrace of ProjectExportFailureTests under load: the JPEG worker's render aborted in LiveMaskGraph::validate). Code a worker runs under a catch that promises to survive memory running out uses std::map and std::set, whose inserts throw: LiveMaskGraph::validate, FolderMaskClip::draw and LiveMaskRenderer's caches (and LiveMaskBaker::bake beside validate, though its workers catch ExportError alone, so a bad_alloc there still ends the app, by design), as ImageExporter::render already keyed its records. The UI thread's own QHashes stay: a failure there ends in Qt's event loop either way. RenderStarvationTests replaces operator new on its thread and fails each allocation of validate, prepareStacks and FolderMaskClip::draw in turn: each must throw std::bad_alloc; the three aborted with the old containers. Painting is left out of it: Qt's painter is not exception-safe, and a failure injected inside it can leave a lock held.
  • Blur and noise adjustment layers, Image Trim and Inner Glow (12.8c, Swift 1.2.3: 98ad817, 24052b5, feb39f3, cd0ca3c). AdjustmentKind gains Add Noise, Gaussian Blur and Motion Blur after Grain, v1.2.3's order (it moved Add Noise ahead of the blurs), with circle.dotted, drop.fill and wind as LayerIcons paths. LayerAdjustment keeps Swift's seven optionals (blurRadius, motionAngle, motionDistance, noiseAmount, noiseGaussian, noiseMonochromatic, noiseSeed), their resolved getters and setters with Swift's defaults (10, 0, 10, 10, off, off, 0), isValid's bounds on every kind, as Swift's, and samplingMargin; the manifest writes each only when set, the seed a UInt32 read as Grain's; the store takes the three kinds from version 9, which it now writes (versions 1–9). apply runs the three through PixelFilter::run with the layer's own seed; FilterJob::noiseOrigin sits last, so the positional initialisers keep their order, and kernels/NoisePixels.c is v1.2.3's (noise_add_at, whose position hash reshuffles the Add Noise filter's grain too). A new Add Noise layer rolls its seed, as Grain does; the editor opens the kind's filter panel on the layer's settings and writes back that kind's alone. Adaptation: Swift's canvas surface holds a pixel a point, so its region's origin and scale are already the surface's pixels; the twin's surfaces hold device pixels, so LiveMaskRenderer::adjust passes adjustmentScale (the canvas's zoom, Swift's, 1 elsewhere) times the device's pixels a region unit, and apply counts the noise origin in the image's pixels, so a HiDPI canvas blurs at its own density and the noise stays one field across dirty rects. AdjustmentSurface::draw takes Swift's padding in the painter's units: the halo is allocated past the shown area, not cut to the device (the canvas's outer surface is dirty-sized), and drawn back under the painter's clip. The canvas pads by the widest visible samplingMargin times the zoom, and its crisp path (from 200%) now draws the layers through a padded surface as well, as Swift's drawLayers wraps its own there. Known difference, Swift's too: on the smooth path the canvas's noise follows the view, as Grain's does; on the crisp path the surface's region starts at the repaint region, so each partial repaint there draws a noise field of its own. The crisp path wraps its surface even without an adjustment, where Swift's drawLayers wraps only for one: pixel-identical (source-over onto a cleared raster), one allocation a repaint region, as the smooth path already does. The HiDPI product is pinned by its two factors (the zoom on the canvas, the device ratio on a scaled composite), not at a device ratio of two itself. Image Trim: Document/ImageTrim twins Swift's file: TrimBasedOn by raw value, TrimOptions (a tolerance per premultiplied channel, alpha included, which the sheet leaves at 0), calculateTrimRect (the alpha bounds, or each row scanned in from both ends against the corner's pixel) and trim (the export's render, then CanvasResizer by the content offset, or the snapshot itself when nothing is cut). UI/TrimSheet is Swift's sheet: 320 wide, the title, Based On as radio buttons, Trim Away's four checkboxes, OK resting while none is checked, Return and Escape through NativeShortcut. ProjectController::trim runs as Canvas Size does, through resizeProject, whose resize may now return nothing: Swift's trim with nothing left returns with no alert and no step. The Image menu's Trim… follows Image Size…, with no shortcut. Left out as dead: TrimError (never thrown), trimImage and the session's trim (called by Swift's tests alone; the twin tests the rect and the controller's two steps), the kept rect's maxX > minX guard (the early returns leave content, so left < right and top < bottom) and the colour scan's last > first (the row's first unmatched pixel stops it). A fix beyond Swift: a raster that cannot be converted is ExportError(render), explained, where Swift's try? reports nothing left; that failure path is untested, since only a failed allocation reaches it. Inner Glow joins as Outer Glow did (12.5b): InnerGlowEffect (10 pixels, white, 75%; 0–500, 0–1), LayerEffectKind::innerGlow last; LayerEffectsKernel blurs the shape at sigma size ÷ 2 and keeps shape × (1 − blur), composed over the colour overlay and under the inner shadow, Metal's order; no margin (Swift's innerGlowRendersInsideSourceWithoutBoundsExpansion), the stroke surface's reach three sizes and two. Swift's CPU innerGlowCoverage is dead here, as Outer Glow's is, so its parity test has no twin. A fix beyond Swift: a reduced preview scales the glow's size. The sheet shows its swatch, Size (0–100, 500 typed, px) and Opacity. Equivalent mutants: the canvas's margin counting hidden layers (it only widens the halo), the glow's opacity > 0 (a clear glow composes nothing; the term spares the blur) and Trim's return of an uncut snapshot (the canvas resizer returns one unchanged at the same size and place). The padding branch keeps an unpadded surface to the shown pixels: mapped there and back, a rect rounds a pixel wider. Tests: BlurNoiseAdjustmentTests (the model, manifest, store, seeds, the editor, the filters at a scale, the noise field, a scaled target, the canvas's zoom and a partial repaint's halo at 100%, 150% and 400%), ImageTrimTests (Swift's six, tolerance, the sheet, the controller through its sheet, a failure's alert), InnerGlowTests (Swift's, but the parity test, and the order), the new kinds in LayerAdjustmentTests, AdjustmentEditorTests, AdjustmentLayerTests and AdjustmentPanelTests, the glow in LayerEffectsKernelTests' replay, LayerEffectsSurfaceTests, EffectsPreviewCacheTests, LayerEffectsTests and EffectsPanelTests, version 9 across the store's tests, Trim in CompositorMenusTests.
  • The finishing filters (12.9a, Swift 1.2.4, e08dd8c): kernels/AdjustPixels.c/.h are v1.2.4's, with adjust_colored_vignette (Camera Raw's falloff pulled out as vignette_mask, Camera Raw's own vignette unchanged) and adjust_tonal_contrast. FilterKind gains Vignette, Bloom / Glow and Tonal Contrast after Add Noise, Swift's order, so the Filter menu lists them; FilterSettings keeps Swift's thirteen fields, defaults and clamps (the colour through AdjustmentColor::clamped). Vignette and Tonal Contrast hand the kernels every setting; Tonal Contrast's base is PixelAdjust::gaussianBlur at radius × scale, clear past the edge, Swift's unclamped applyingGaussianBlur. Known difference: Core Image's CIBloom is private, so the twin's Bloom / Glow is the pixels plus their Gaussian glow (radius × scale, clear past the edge) at amount ÷ 50, Swift's intensity, added byte by byte on the premultiplied values and rounded once, alpha included, so the glow shows past a clear layer's light as Swift's test asks; Core Image's own curve may differ. Bloom grows the grid by three radii and two and is trimmed on commit, as the blurs are. A commit with nothing to change closes as Cancel does: Vignette or Bloom at amount 0, Tonal Contrast at amount 0 or with all three tones at 0. Every filter's commit now keeps the layer's effects (Swift's rebuild gained effects: current.effects); shape and text still drop. FilterEdit::previewSourceVersion is Swift's: prepare moves it with each new grid, the worker hands back the version it rendered from, and a result for an older grid lands nothing, its waiting job rendering next (a render begun before growForBlur would otherwise show the old grid's pixels in the new placement). The twin's one watcher can hold a job waiting behind another edit's worker, which Swift's per-edit task never does, and Preview off keeps that job: a growth therefore rebuilds a waiting job on the new grid, so the version it starts with names its image. The picker gains Swift's Vignette target (openVignetteColorPicker, previewVignetteColor, "Color Picker (Vignette Color)"), closed with its colour by OK and without by Cancel, as the Gradient Map's; Swift's committing terms are left out as the Gradient Map's are (a commit keeps the project busy and closes the picker first), and so is setVignetteColor's edit guard, read through value(): the Vignette picker lives only while its edit is open (Cancel and OK close it first), where the Gradient Map's may outlive an adjustment's editor. UI/FilterSheet+Finishing.cpp holds the three kinds' rows, FilterSheet.cpp running to 442 lines: Vignette's Color row (a 95-point title, the Gradient Map's 24-point swatch), Amount and Highlights with Swift's help, Midpoint, Roundness (no unit), Feather; Bloom's Amount and a logarithmic Radius; Tonal Contrast's Amount, Shadows, Midtones, Highlights and a logarithmic Radius. The sheet hands the picker's working colour to the Vignette and repaints the swatch on every change. Tests: FinishingFilterTests (Swift's five, then the kernels at a scale, the settings' bounds, the idle commits, Bloom's grid, a preview for an older grid, the picker, the sheet), the kinds in FilterModelTests, a filter keeping effects in LayerEffectsTests, the Filter menu in CompositorMenusTests.
  • Keyboard zoom, the rail and the tab strip (12.9b, Swift 1.2.4: 383548c, 8053fb6, 75c4219). CanvasViewport::keyboardZoomLevels holds Swift's seventeen stops (12.5% to 1600%) and keyboardZoomTarget the next stop past the zoom within a billionth, or the zoom itself at an end; EditorSession::zoomKeyboard steps about the view's centre, leaves fit mode and stays silent when nothing moves; Swift's step != 0 there is dead and left out (the viewport answers the zoom itself for a step of 0). The View menu's Zoom In and Zoom Out and the toolbar's zoom buttons step through the stops, the menu entries doing nothing while a text field or open text holds the keys (Swift's NSText guard; the entries' m_field and m_canvas with a draft). Swift's handleKeyboardZoom catches its zoom keys on key-down and repeat because a Mac menu key equivalent does not repeat; Qt's shortcuts repeat, and a matched shortcut never reaches a key filter. The menu's Ctrl+= and Ctrl+- also take Ctrl with the keypad's minus (probed in the app), so the canvas's window-wide filter (handleKeyboardZoom, beside the blend keys, outside text) takes only what Zoom In's entry cannot hold: Ctrl with Shift and = (Qt's Ctrl++) and the keypad's plus, never with Alt or Meta, and only while that entry keeps its default key (a remapped entry leaves the plus alone, as Swift's check of ShortcutSettings). Swift's zoom-out chords are all the menu's here, so the filter has no zoom-out branch. Swift's 8053fb6 pads the zoom buttons by 4 points; Qt's toolbar spaces its actions itself, left as it is (cosmetic). The rail already scrolls without scroll bars at 56 points (4.3), what IndicatorlessScrollView gives macOS. The tab strip follows ProjectTabScroller: each title button is Swift's projectTabLabelWidth wide (the title's width in its 12-pixel medium or semibold font, 10 more when unsaved, 35 to 155) plus Swift's trailing 8, the title elided within it and painted from the left (TabTitle, Swift's leading frame; a QToolButton centres its text), so a title moves only the tabs after it; the unsaved dot is a character, wider than Swift's 10 points of dot and gap, and widens its button by the excess (known difference). A new front tab scrolls into view, posted as Swift posts its reveal, once the row is laid out; any other change keeps the offset, which Qt clamps, where the strip used to follow every layout (Swift's drop slot has no twin, 4.7). Tests: KeyboardZoomTests (Swift's five, the tolerance from each side, and the key test in the app's own window: each chord's route, repeat, Shift-minus, Alt, Meta, text fields, a remapped entry, no document), ProjectTabStripTests, the menu in ViewMenuTests, the toolbar and strip in ProjectWorkspaceViewTests.
  • The format version in one place and the layer context menu (12.10a, Swift 1.2.5: 4cc9951; 6fa115e, 2f1e796, 5c5f05b, 1366f68, 5d7adba). ProjectManifest::current (9) is what saves write and supported (1 to it) what loads accept; the manifest's default, both version checks (supports) and the version error's range read them, so a bump is one edit. Swift moved the layer menu from each cell to the table; the twin moves it from LayerCell::actions() to the list: NativeLayerList::contextMenu is Swift's contextMenu(for:) (a row pressed with nothing selected is selected first; Duplicate Layer, Rename… for one layer alone, Delete Layer, Delete Selected Layers or Delete Mask, Create or Release Clipping Mask, Group Selected Layers, Move Out of Folder, the merge entry under mergeTitle, Add Mask with Reveal All (White) and Hide All (Black), Enable or Disable Mask, Delete Mask, Link or Unlink Mask, Show or Hide Layer, with Swift's separators and each entry enabled as Swift's validateMenuItem, the mask entries targeting the pixels first; 1.2.5 left Layer Effects out; a selection refused (Levels open) leaves no active layer, and every entry then rests); menuFor is Swift's menu(for:), routing the press first: on an effect's row that effect is chosen (Swift's selectEffect(at:)), elsewhere the chosen effect goes, a row counts as selected only while no effect is chosen (selectedEffect, which a stale choice an undo left fails, as the row's painting reads it; Swift's table then shows none), the pressed row's id is read first (Swift's targetLayer), since a selection may finish text and rebuild the rows, a selected row keeps the selection with the pressed layer primary unless a thumbnail targets its mask or pixels, and an unselected row becomes the selection, the list takes the keys and its mask or pixels are targeted. LayerCell::contextMenuEvent executes that menu; below the rows none shows. Left out as dead: Delete Mask's mask != nil (the session chooses a mask only while it exists) and the routing's effectSelection = nil (selectLayers and selectLayerTarget, which every path calls, drop a chosen effect themselves). The entries act on the layer active when chosen, as Swift's @objc actions read it, where the old cell menu acted on its own row; Rename asks canEditLayers again then, as Swift's renameSelectedLayer. Tests: LayerContextMenuTests (Swift's regression tests, the folder duplicate included; titles; a real right-click), LayerContextMenuEdgeTests (the gates at their edges, a refused selection, action-time targets and an entry with no active layer, a chosen or stale effect, a rebuild under the press), LayerContextMenuFixtures.h, theCurrentFormatVersionIsOneTheReaderAccepts in ProjectTests, the old cell-menu cases in NativeLayerListTests and LayersPanelTests moved to the list's menu.
  • Whole layers on the clipboard and several duplicates (12.10b, Swift 1.2.5: 5ef5b0a, c873453, 245db93, 2f1537f). CopiedLayer (in Document/SelectionClipboard.h) is the session's copiedLayer: Copy with no selection and no chosen mask (canCopyLayer) takes the selected layers whole; a layer with pixels still copies them as well, for other apps, and a folder or adjustment puts only its id on the clipboard under Swift's com.compositor.copied-layer, dropping the pixel copy. Any copy of pixels drops it; it is judged as a pixel copy is (clipboardHolds), so a copy anywhere since, in this app or another, ends it for good, a history entry set again included, and Wayland's second announcement of the same copy does not. The copies' active one is the active layer's copy, else the first id's with a copy in order (Swift's values.first is any). ProjectWorkspace::pasteCopiedLayer finds the tab whose copy still stands, keeps the ids its document still holds, and duplicates them in place as one Paste step, refused while that project cannot edit its layers, or copies them into the front tab through copyLayers; false leaves Paste to pixels. The Edit menu's Copy runs with canCopyPixels or canCopyLayer, and Paste asks the workspace first, a text field and open text before both. Ctrl+J without a selection duplicates a folder too (layerViaCopy), and its entry is enabled for any active layer; a folder under a selection copies nothing. beginDuplicateTransform stacks its copies through duplicateLayers, the copies being what is selected after it that was not before, the active copy leading. The copy between projects moved to ProjectWorkspace+Copy.cpp, since the workspace's file reached 500 lines. Tests: LayerClipboardTests (Swift has none), the updated folder cases in SelectionClipboardTests, CompositorMenusTests and TransformTests, and effectsTravelWithTheCopy in ProjectCopyTests.
  • Crop, Vignette and text colour from Swift 1.2.5 (12.10c: 8b5a6af, 5ece82b, 0488cab, 3317d02, 4e5f4ec). The crop ratios gain 3:4 and 9:16 (cropRatio, CropControls). Choosing the Crop tool with a selection that is not empty starts the frame at the outline's box (boundingRect, which leaves control points out as CoreGraphics' boundingBoxOfPath does; controlPointRect would not) rounded out to whole pixels (integral) and cut to the canvas, the canvas when that is no valid frame (an outline nudged wholly off it; an empty line between rows, which rounds to a row, stays out through isEmpty). Vignette opens round (vignetteRoundness 100, the default a number that is none falls back to). On an empty layer (no pixels, no adjustment, no folder: canVignette, the gate the Filter menu's entry reads) it begins from clear pixels the size of the layer's box, grows its grid to the canvas and frames the canvas (FilterJob::canvas, mapped into the image's pixels through the inverse mapping; top-left rows, so Swift's flip is gone), painting clear pixels too (AdjustPixels.c's fillsClear, Swift's kernel as it stands); on a layer with pixels it frames them and recolours only them, as before. The commit lands on a layer still without pixels (startedEmpty), and the canvas draws an empty layer while a filter previews it. canAdjustColors and canVignette share canAdjust(allowingEmpty); Swift's separate adjustment == nil and isGroup terms are the twin's argument and canAdjust's own. The text colour previews while a picker is open: the Type bar's picker and the foreground picker opened over open text (ColorPickerState::editedText, the draft and its colour), through previewTextColor, which the Type bar calls on every change of the working colour (Swift's onChange), and only on the draft the picker opened for; Cancel puts the draft's colour back, OK keeps it and moves the foreground, and a Cancel with no text open leaves the next text's colour. Swift's background: false term in previewTextColor is dead (only the foreground picker records editedText) and left out. The tab strip's edge fade (1fb6a3f, e8ed83b) has no subject here: the twin's strip draws no fade, and it sits in a QToolBar under the desktop's own title bar, which drags the window itself. Tests: Swift's textColorPickerPreviewsAndRestoresDraft and theForegroundPickerPreviewsOpenTextToo in TypeToolTests, thePickersColourPreviewsOnOpenText in TypeControlsTests, theFrameStartsAtTheSelection and the ratios in CropTests and CropControlsTests, VignetteCanvasTests (the kernel's frame and fill, the gate and menu, an empty layer's commit and canvas preview, a layer with pixels), the defaults and the kernel's arguments in FinishingFilterTests.
  • Limits, ants and wheels from Swift 1.2.8–1.2.10 (12.11a: 7dadc88, bc865fb, 8eba077, 96edd5f). Document/DocumentLimits twins Swift's enum as a namespace: maxSide (30,000), maxSurfacePixels (200 million, one canvas, export, filter target, layer, mask or generated surface), documentPixelBudget() (every layer, and apart every mask, of a document together: a sixteenth of physical memory in bytes, Swift's physicalMemory / 16, read once through sysconf, between one surface and 800 million) and the megapixels the messages quote; budgetFor(memory) is the budget's formula alone, so tests reach every memory size. Swift's maxSideExtent and maxSurfaceExtent (CGFloat copies) have no twin: C++ compares across types. Each literal Swift replaced is replaced as Swift's diff does it, the surface ceiling or the document budget, and the twin-only ones as their Swift counterparts: the RAW import's remaining pixels and the effects kernel's ceiling. Messages print the numbers (maxSideText, English grouping as the status bar). Tests fill budgets with layers that claim pixels they do not hold (tests/BudgetFixtures.h: claimed, claiming), so a boundary costs no memory whatever the machine; where an import or a save would still make the pixels, a boundary is read where it is judged before any pixel: the resizer judges a layer before it looks for its image (a missing image then says the budget passed; a canvas widened in one direction puts a layer on exactly the remainder), the store judges a file by its PNG header on load (claimingPNG, a header with a size its pixels never fill) and by the asset's size on save, before flattening it, as Swift reads a lazy CGImage's size unrendered (the twin flattened first until 12.11a; a mask both invalid and oversized is now refused as too large where Swift says invalid). Save and load count through one checkSize(size, isMask, budgets), images and masks apart. Every mask the resizer passes is drawn, so its exact edge is read under AddressSpaceLimit: the budget passes and the surface then finds no memory (ExportError(render)), one pixel more is refused as too large. The marching ants (Rendering/TransformOverlay+Ants.cpp, Swift's level of detail): an outline past 20,000 path elements, drawn below one screen pixel a document pixel, is filled into a gray mask (at the step at least, else as fine as about 40 megapixels allows, Swift's formula and its comment's bound alike; the path's own fill rule, where Swift's are winding and the twin's paths keep theirs), averaged down to its power-of-two step (SmoothTransformation for CoreGraphics' medium interpolation), any coverage counted, and traced along its pixels' edges through MagicWand::outline, in a QtConcurrent job whose watcher the overlay owns: a new source or step replaces the watcher, so an older result drops, and until a step lands the last one stands in, none at first. A trace whose tracer throws (MagicWandError) logs on omaphoto.rendering where Swift's try? is silent. The overlay asks the canvas to repaint the ants when a trace lands (repaintAnts); the canvas skips a tick while its last repaint is pending (m_antsRepaintPending, cleared as it paints: Swift's needsDisplay guard). The grading wheels' direction (96edd5f) came with 12.8b2: Qt's conical gradient already runs counterclockwise, and theWheelsHueRunsCounterclockwise holds it. Equivalent mutant: the landing's repaint (the ants timer repaints the same rect within a tick while a selection shows). Tests: DocumentLimitsTests, MarchingAntsTests, and every test that pinned the old ceilings, now read from DocumentLimits.
  • Photoshop Large Documents, background-only files and oversized layers (12.11b1, Swift 1.2.8–1.2.10: b8e3cd1, e6bcfa1, 1fcf854). Swift's PSDReader.swift runs to 543 lines, so the twin splits it: IO/PSD/PSDReader+Records.h/.cpp hold the cursor, the raw records, their pixels and the cut (PSDRecords), PSDReader.cpp the header, the assembly and the merged image. A version-2 header is a Large Document (Header::isPSB): the layer section's and layer info's lengths, each channel's length and the additional-info keys Swift lists (psbLargeKeys) take eight bytes (Cursor::length, a length past qint64 too large), and PackBits rows count in four bytes (PSDChannelCoder::decode's largeDocument); another version is unsupportedVersion, whose message names no format now (Swift's words, OmaPhoto for Compositor). The import panel offers *.psb, a dropped picture prefers the PSB type right after PSD's (ImageFileDrop, Swift's order), and the MIME package declares image/x-photoshop-large-image (a *.psb glob and the version-2 signature as magic) for the desktop entry, as Swift's Info.plist declares the type. A file whose records do not fit the budget together (fitsBudget, the check decodeChannels makes, summed) has every layer and mask cut to the canvas (cropToCanvas, the bounds Swift's crop gives, the channels' source bounds kept) and is judged again, refused only if it still does not fit; a cut channel is decoded straight into its part (PSDCrop: raw rows copied in part, PackBits rows outside it skipped by their counts, one routine with the whole image as its crop for an uncut channel), and each cut layer's report says so (PSDRecord::croppedToCanvas, the note first in its layer's notes). A file with no layer records, Photoshop's background alone, reads as its merged image (PSDReader::merged, ImageIO's read for Swift): the header, its ICC profile (resource 1039) attached as Qt reads it and sRGB when it has none, the sides and the remaining budget judged from the 26-byte header before the file is read, its ICC profile (resource 1039) copied only for this read, since a layered read skips it as Swift's does, as Swift asks ImageIO's properties before it decodes (the layered canvas's one-surface ceiling does not apply, as those properties know none), the first three of its channels' planes from the image data section (every channel's row counts precede the rows), opaque; ImageImporter::decode's flattenedPhotoshop sends it through the importer's own colour conversion and thumbnail, and when the Photoshop worker finds no records (PhotoshopRead::layerless) the session ends the reading first, as Swift does, then decodes the merged image through the ordinary decoder (decode(url, flattened), which also takes the merged reader's PSDError and ExportError) and inserts the one layer without the conversion sheet. Known difference: ImageIO also reads 16-bit and non-RGB merged images; the twin's reader keeps the layered path's 8-bit RGB rule. The second fitsBudget refuses before any pixel is read (decoding the first layer could fail otherwise, and differently), and the crop's max cuts an inverted record to nothing, reported. A source's bounds and the cut stay 64-bit (PSDCrop, the coder's widths): once a cut makes a source past the side limit acceptable, its width can pass int, and Swift's Int holds it; PackBits rows decode straight into the crop's columns, so no row the width of the source is held, and the counts table is judged against the data before it is allocated. A PackBits row past the crop writes through a checked std::span, so a lost skip aborts. Tests: PSBImportTests (Swift's three; the merged image, its profile, budget and surface rule; the session's import at a point), CropToCanvasImportTests (Swift's five; the layers summed before any cut, a layer wholly before the left edge), the version and oversized cases in PSDRoundTripTests and PSDReaderLayerTests, the MIME package in DesktopIntegrationTests.
  • Photoshop text (12.11b2, Swift 1.2.10: b8cce6e, fa2b342): IO/PSD/PSDText.h/.cpp twin Swift's file, a namespace; Swift's 682 lines split into PSDText+Engine (the text engine's PostScript dictionary, a std::variant tree read through walk, number, boolean, string and array) and PSDText+Descriptor (Adobe's action descriptors, Reader and the item lookups; a rectangle is a normalised QRectF, and a side's key is chosen by presence before its type is read, Swift's ??). A type layer (TySh, else tySh; 8,000,000 bytes at most) reads as Swift's: version 1, a finite matrix, version 50, the text descriptor; vertical text, shear and uneven scale refuse (the matrix's scale alone maps engine sizes to pixels, never the document's resolution); a warp other than none, faux bold or italic, runs whose twelve-field signature differs from the first, and justification other than left, right or centre each add Swift's note; the words come from Txt (else Txt), cleaned of a leading BOM or NUL, trailing NULs and CR line ends, and only when that item is missing from the engine's EngineDict/Editor/Text; 1 to 100,000 UTF-16 units. A frame (bounds) wider and taller than the letters (boundingBox) by more than four makes box text, its corner mapped through the matrix; rectangles read standardised, as CGRect reads them. render draws through EditorSession::textImage and places the anchor (the frame's padded corner, or point text's first baseline at its alignment's x, TextLines::baseline, new here) on the document point through the layer transform, a transform that is not valid being ProjectError(tooLarge). The reader parses type first, so a parsed type layer never becomes a live shape or a rasterized vector mask; the builder draws the text (any std::runtime_error keeps the pixels with Swift's rasterized note, Swift's try?), reports the parser's notes and a missing face, and makes a live text layer. A missing face is one fontconfig does not list by PostScript name (TextLayout::availableFonts), where Swift asks NSFont; its note says the text was drawn with a substitute font, since Qt draws fontconfig's match where macOS draws the system font. Engine numbers read as Swift's Double(text) (whole token, C locale, overflow infinite, through strtod_l); a name with a high byte reads empty and an identifier with one refuses, Swift's ASCII decoding; an odd count of UTF-16 bytes in an engine string reads empty, and a lone surrogate in either reader becomes U+FFFD (Qt's decoder keeps it; Swift's strings repair it), a valid pair kept. Fixes beyond Swift: nesting deeper than 512 in either reader refuses the block, where Swift's recursion overflows its stack, and a font index or justification past Int64 reads as absent (left, with the note), where Swift's Int() traps. Left out as dead: placement's finite-scale guard (a scale over 1e-6 from finite numbers), parse's font-size guard (every path clamps it), the inner frame guard (the final isValid refuses the same style), baseline's empty sample and no-glyph fallback (the words are never empty, and point text's image holds its first line), the frame's width > 1 and height > 1 (each side already passes the letters' by four) and Double(text)'s empty token (a number starts with a byte it keeps). Equivalent mutants: runs.size() > 1 as > 0 (it only keeps begin() + 1 off an empty list), rounded's Int64 bound (past it the cast is undefined, and x86 yields a value no index or justification matches), the string's end after a lone backslash (the dictionary cannot close then) and the retry's start > 0 (a retry at zero fails again). Tests: PSDTextTests (Swift's ten, the builder's notes, rendered pixels and exact transform, the matrix's two per cent, each signature field, a rendered frame's corner, the baseline in ink, frames placed and flipped, the style's fallbacks, a type layer with a shape), PSDTextReaderTests (the engine's forms, deep nesting, every descriptor type and cap, the list and reference counts, nesting and names, lone surrogates, colours in one, three and four channels, each channel as a second style, the sources' precedence, rounding, scaled leading, the Txt key, negative exponents, both versions and the limits at their edges); PSDFixture+Text.h twins tySh, and PSDFixture::data now takes several additional blocks.
  • Text at the click, text on the Move tool's double click, and SVG (12.11c, Swift 1.2.10: 292cb6b, 072fa08, a5721b2, 0534e00): a click with the Type tool puts new text's first baseline on the pointer, the box its padding to the left and its first baseline above (TextLines::baseline(0) of the style, the laid line's descent, where Swift reads the font's descender: they agree within a hundredth of a pixel); a dragged box sits exactly where it was drawn. Swift's setHiddenUntilMouseMoves(false) has no twin: Qt never hides the pointer while typing. The canvas's press is press(event, clicks), Swift's mouseDown with its click count: mousePressEvent passes one and a double click's fall-through two, so a triple click counts one (the Type tool keeps its own count); with the Move tool a second click on the topmost shown live text under the pointer (a hidden folder hides what it holds) runs commitTransform, selects it, opens its editor and synchronizes the inline editor (beginLiveTextEdit, in EditorCanvas+Transform.cpp), else the press goes on to guides and the transform drag. No transform edit is open there (canEditLayers), but commitTransform also ends an opacity drag and a blend preview, which selectLayer leaves when the text is already active. In the layer list a live text row's size column says "Text · Double-click to edit"; a double click on its thumbnail opens it and on its name renames it, as Swift's merge (3258c66) kept, where the branch commit opened text anywhere. Opening existing text puts the caret after it once the editor takes the keys, unless a click already placed it (that merge's step in InlineTextEditor.synchronize): the queued focus request moves an untouched caret (0, 0) to the end, for a draft on a layer alone; the editor keeps its canvas as a CanvasView to reach itself there. SVG goes by its suffix, svg or svgz in any case (the importer's filter lists both, the desktop entry image/svg+xml and image/svg+xml-compressed, which shared-mime-info keeps apart) (Swift's UTType.svg), after camera raw and before Photoshop's bytes, through the ordinary decoder with the canvas's size: ImageImporter::decodeSVG renders once through Qt SVG's QSvgRenderer (a new dependency: qt6-svg-dev in the dev image, qt6-svg, qt6-qtsvg-devel and qt6.qtsvg in the distro images, the PKGBUILD and the flake; the DEB and RPM find the library themselves), fitted to the canvas by the smaller ratio, else at the declared size, each side rounded and one pixel at least, refused past the side limit or the remaining budget, tagged sRGB, named without its suffix. Drops need nothing, since a dropped picture of any image/ type already imports. Known differences: Qt SVG draws SVG Tiny 1.2 where macOS's renderer draws more of SVG 1.1 (filters, masks, some CSS), defaultSize truncates a declared size to whole pixels (40.9 by 20.9 draws 40 by 20; a side that truncates to nothing falls back to Qt's bounds, which round, so a declared side under half a pixel refuses and one from half a pixel draws a pixel wide, where Swift draws any positive side a pixel wide at least), and a drawing whose declared size is empty takes its content's bounds, where NSImage measures nothing; Swift's isValid guard is folded into the size test, since Qt measures a drawing it cannot read as nothing. Tests: Swift's two TypeToolTests changes and the baseline in ink (clickedTextSitsOnItsBaseline); LiveTextDoubleClickTests (zoomed and turned text, a hidden folder, the cleanup, the caret); SVGImportTests. Tests that set text up at a known corner use beginTextAt (SessionFixtures.h), which pins the draft's corner as Swift's clipping test does.
  • Typed text as pixels, effects kept on (12.12a, Swift 1.2.11: fe6cb2b, b683acb): the canvas draws text being typed as the pixels textImage makes of the draft's style (remade only when the style changes and let go at the first draw without a draft, as Swift's drawNewText asks with no draft, each remake a debug line on omaphoto.rendering, a style that cannot be drawn logged and drawn as nothing), where the editor shows it: an edited layer in its place with its opacity and blend mode (its mask left off, as Swift's), new text just above the active layer, or on top when the active layer is not drawn (a folder, a hidden or blank layer, none). The editor's glyphs are clear: it draws its frame, handles, caret, marked text's underline and a selection at 45% of the highlight over those pixels. An underline without a colour of its own, the preedit's or an input method's, takes the text's colour, since clear glyphs would hide it (Swift's NSTextView draws marked text itself). DisplayState carries the draft's style and shown transform, so typing repaints the whole view, as effects reach past the box; a handle drag at a zoom can move the box a fraction with the style unchanged, so the transform counts on its own. The edited layer's effects are redone as it is typed, synchronously at the preview size (EffectsPreviewCache::renderNow, the worker's scaling moved into scaled), through its enabled mask or its placed mask clipped over the typed box; until a change is redone the last effects stand in. When the edit ends and the layer holds the text as last typed, the next draw seeds those effects (handOnDraftEffects); a cancel of changed text, or other text, seeds nothing. The cache keeps the last three finished results per layer: pixels an undo puts back show their effects at once, a known result clearing any seed; a mask added, removed or replaced on the same pixels keeps the last preview until the new one lands, where it dropped before. A painted mask keeps its layer's effects through the stroke: LayerEffectsSurface::MaskStroke holds the stroke's tiles, their map into the surface's grid and the coverage of any region (the old mask, then the tiles over it, as a tile already holds the old mask it covers; Swift's white fill beneath is dead, since past the old mask there are no pixels), and the surface redoes the regions the mapped tiles touch; a placed mask's surface keeps the layer's grid and takes coverage from placedMaskPreview (placedMaskSurface). Swift's restoreToolCursor has no twin: syncPicking already sets the cursor both ways, and a widget's cursor cannot be left over from another window. strokeDirtyRect moved to EditorCanvas+Brush.cpp, the typed text to EditorCanvas+DraftText.cpp. Left out as dead: the cache's filter of a result already recent (a request matching one returns before it renders). Equivalent mutant: the mapped tile rect's one-pixel inset (the redo region is widened by the effects' reach). Tests: TypedTextCanvasTests (Swift's textLooksTheSameWhileEditingAndOnceCommitted at 1.5 and 4, with the box's inside compared since the editor's frame draws on the canvas; placement above the active layer, opacity and a blend mode with and without effects, effects kept and handed on, masks linked and placed, cancelled text, remakes counted and the image let go once editing ends, typed effects redone when a handle moves a placed-mask box a fraction, the whole view repainted when typing or when a handle moves the box a fraction), MaskEffectsCanvasTests (linked and placed masks keep effects mid-stroke, a placed mask sixty times finer against a whole render; untouched tiles keep the old mask), EffectsPreviewCacheTests (recent results, renderNow, the looser keep), LayerEffectsSurfaceTests (a mask stroke redone where its tiles map), and the inline editor's tests read the 45% selection through highlightedOver.
  • Masks painted anywhere (12.12b, Swift 1.2.11: e753f26, 8d2eade, 2ffb092, a247676, taken as their net change from 1.2.10): BrushStroke takes growsMask, which the Brush, the Gradient and fills (applyPixelEdit) pass, so on a mask their grid reaches the canvas as a pixel stroke's does; Spot Healing, Clone Stamp and the Smear keep the mask's grid. A solid placed mask (two pixels a side at most) is painted at a pixel per document pixel of its placement rather than grown from its pixel. maskBackground (LayerMask::background of the old mask's thumbnail, white or black, 1 without a mask) fills each new tile, the committed snapshot past its base and patches (RasterSnapshot::fill, drawn where neither reaches, the box of a mask grown by distant tiles holding such corners) and the asynchronous commit's context (BrushCommit::Input::fill). A mask stroke's snapshot keeps every touched tile (committedBounds). Committed, a mask grown past its old bounds, or placed already, takes the stroke's transform as its placement (placedMask, for the snapshot and the raster commit alike), keeping its place on the document while a linked one still moves with its layer. On the canvas an unplaced mask stroke draws in its possibly grown grid (paintTransform), a placed one in the layer's place, and a placed mask's preview and surface map through the stroke's paintTransform. CanvasThumbnail::mask fills past the mask with LayerMask::background, so a stroke touching the edge leaves no gray mean; the thumbnail's own edgeTone is gone. 8d2eade's canvas fix (the distortion's effects asked with the mask as stored) needs no twin: the twin asks for a layer's effects once, with the mask as shown (9.6c), and its y-flip in LayerEffectsSurface has no Qt counterpart. The linked mask surface's white fill (12.12a) stays dead: the layer's pixels lie only within the old mask's rect, however far the grid grows. Equivalent mutant: placedMask's mask.placement term (a placed mask keeps its placement through replacing, and its transform over unchanged bounds is that placement). Tests: MaskPaintAnywhereTests (the brush grows a mask and keeps it linked and in place, a hide-all mask's new area hides, the Gradient and a fill grow it, the Smear's blur does not, a solid placed mask's grid (two pixels a side, a box not square, grown and not), the snapshot's and the commit's fill in uncovered corners, a grown stroke drawn in place, a grown placed mask's tiles mapped under effects), CanvasThumbnailTests (Swift's edge case), and MaskEffectsCanvasTests' finer placed mask through the stroke's transform.
  • Scrubbable labels (12.12c1, Swift 1.2.11: 2d36d6e, 680a522): UI/NumericScrub twins Swift's modifier as a QObject living as its label's child and filtering its events; scrubbable and unitSuffix(_:scrubValue:) are its constructor on the label or unit (Options: sensitivity, range, optional step, value, set, onStart, onEnd). A left press on an enabled label, a double click's second press included, is taken (a disabled label's presses reach its filter before Qt drops them, so the filter asks), so the label keeps the drag past its edge; the first move off the press begins (Swift's minimumDistance of one, reached by any move of whole points), keeps the start value and calls onStart; each move sets the start plus the sideways distance (in the label's own points, which stay exact, where Qt's notify rounds global ones) times the sensitivity, snapped to the step when positive, then clamped; the left release calls onEnd. Swift's Int overload is a step of one. The cursor is the label's own SizeHorCursor: Qt keeps a grabbing widget's cursor through the drag and shows a disabled label's parent's, so Swift's hover and restore dance has no twin. Qt can lose a release: the left release is watched application-wide while pressed, and a move without the left button ends the drag (the label tracks the mouse, so such a move arrives). The tool bars scrub as Swift's: the Brush's Size, Hardness, Opacity or Strength and Smoothing, the Gradient's Opacity, the Magic Wand's Tolerance, Object Select's Edge, the Expand, Contract and Feather units (Expand's and Contract's rest with their rows; Feather's amount and unit stay enabled without a selection, its button alone resting, as Swift's, which the twin's amount had missed), the amount sheet's title (writing its input), the Shape's Width and Radius (a scrub writes its field's text over any typing, as a step does: showValue on SelectionAmountField and the Type bar's TextStyleField), the Type bar's size unit, Tracking and Leading (whole numbers), the Move bar's labels (whole numbers in Swift's new ranges: ±30,000, sides 1 to 30,000, Scale 0.1 to 30,000%, rotation ±360; the scrub writes its field's text over any typing, then the value, so an unfocused field shows what the session keeps; another active layer ends each field's drag under way, TransformValueField::endScrub, as it ends the opacity's), the zoom's unit (through the field's step) and the layer opacity (one opacity drag, beginOpacityEdit to finishOpacityEdit; another active layer ends a drag under way through NumericScrub::end, since the twin keeps the controls where Swift's .id remakes them, and later held moves wait for a fresh press). The sheets and panels follow in 12.12c2. 075a71b's narrowing fix in AdjustPixels.c is taken (the kernels match Swift's); its other changes and 725b934's are Swift's concurrency, plist and CI, with nothing to twin. Swift's emptyCanvasLeavesTheScrubberCursorAloneAndRestoresTrackingForADocument has no twin: the canvas never sets a global cursor, only its own. Equivalent mutant: removing the application filter when a drag ends (it then sees nothing to end). Tests: NumericScrubTests, ScrubbableLabelsTests, ScrubbableTransformTests (the Move bar, the zoom unit, the layer opacity), ScrubFixtures.h.
  • Scrubbable titles in the sheets and panels (12.12c2, the rest of 2d36d6e and 680a522): each title (or channel letter) is a NumericScrub with Swift's sensitivity, range and step, and every scrub replaces its field's typing (setModified(false), as the row's slider does) before the sheet shows the value. Effects' rows scrub to the typed limit (Swift's inputRange, the stroke to 500); Hue/Saturation's titles take their range from Colorize, the Canvas and Image Size titles theirs and their sensitivity from the unit, Relative, Lock, Resample and the resolution, so the owner hands new limits to NumericScrub::reshape on every synchronize, as SwiftUI's next render hands the modifier new ones. Levels' captions move a tone a level a point and gamma a hundredth, within 0 to 255 and 0.1 to 9.99; a filter's title moves the field's last decimal a point (1 / 10^decimals) over the linear range, logarithmic sliders included, as Swift's; the colour picker's R, G and B move whole levels. Canvas Size snaps to whole units in pixels, percent, inches or centimetres, its range Swift's scrubRange (a locked ratio keeps both sides within 1 and 30,000; Relative counts from the current size); Image Size's titles rest while a physical unit has no positive resolution (Swift's canScrubDimensions), resampling spans Swift's pixel and hundred-megapixel limits, a print size moves a hundredth of an inch (0.0254 cm) a point down to the pixels at 9,600 per inch, and Resolution scrubs whole pixels per inch from 1 to 9,600, through the same setResolution a typed one takes (a physical size keeps, its pixels scale). Camera Raw's rows opt in through CameraRawRow::Spec::scrub (the sensitivity), scrubbing through the row's typed setter, unrounded and unclipped, as Swift's bindings do: the main rows a point per last decimal, the curve's amounts and Refine Saturation, the HSL family titles (the Color and Point Color rows stay passive, as Swift's colorSlider and pointSlider), Detail, Optics, Geometry and Calibration a unit a point; Color Grading's Blending and Balance do not scrub, as in Swift. A title's double-click reset runs first and lets the press through, so the second press still scrubs, as Swift's simultaneous gesture does. A sheet destroyed while one of its fields holds the focus used to commit into its destroyed members; 12.14c fixed that (releaseFocus), though the app hides a panel's content before deleting it anyway. Equivalent mutant: the colour channels' step of one (the picker's colour is quantized to 8 bits, so a channel lands on whole levels anyway). Tests: ScrubbableSheetsTests, ScrubbableCameraRawTests (each row group's value, setter, sensitivity and limits; the passive titles).
  • Projects changed on disk (12.13a, Swift 1.3: ed5ddea, cd4061a, cba6efc): IO/ProjectWatcher twins Swift's class with a QFileSystemWatcher (inotify) on the package folder, its manifest and its images folder, where Swift opens kernel vnode sources: an event restarts a 300 ms single-shot delivery (Swift's coalescing) and re-arms every path from scratch each 100 ms, up to 20 times, until all three are watched (both timers are named children, delivery and rearm, so tests read them, and precise, since a coarse timer may fire 5% early where Swift's sleeps never end early; so is the controller's recheck); Qt's inotify engine subscribes to no IN_MOVE_SELF, where Swift's sources watch .rename, so a package exchanged for another one kept aside would pass unseen: the watcher also watches the package's parent folder and counts an event there only when the package path's device and inode differ from the ones it armed on, because a package swapped in (our store's renameat2 exchange, another app's rename) retires the watched inodes. IO/ProjectDigest is a namespace: SHA-256 (QCryptographicHash) over the manifest's bytes, then each regular file in images/ (links and folders skipped, names sorted by UTF-16 units) as its UTF-8 name and its size as a little-endian 64-bit number; a manifest that cannot be opened, or reads short of its size (a failed read, as Swift's Data(contentsOf:) throws), or an image gone mid-listing, throws, which the callers read as no digest (Swift's try?). ProjectController+ExternalChanges.cpp holds Swift's extension, its state ExternalChangeState (ProjectController+ExternalChanges.h) a public member, as Swift's: the watcher, the known digest, checking, pending, saving, the recheck QTimer (single-shot, a child of the controller), recheckAttempt and reloadCount. Swift's async loop runs as callbacks: each pass takes the pending flag, stops without a path, a document or while our own save writes, computes the digest off the UI thread (a QFutureWatcher child of the controller, so an answer dies with it), passes over a digest that matches, backs off at 250 ms × 2ⁿ (n to 7) while the project cannot start an operation, a transform is open or the workspace manages, reloads an unmodified document, and for unsaved work asks Swift's alert (Revert, Keep Mine) over the window, only for the front tab; without a window, or behind another tab, the change waits for resumeExternalChangeCheck, which the workspace's select calls. Keep Mine remembers the new digest. A reload (reloadFromDisk) marks the project busy, loads through the store off the UI thread, lands only on the same path with a document still open, calls EditorSession::reloadProject, remembers the digest as loaded and then counts the reload; a package that fails to load leaves the document alone and logs on omaphoto.io. reloadProject installs the snapshot and puts back the viewport, the collapsed folders that still exist, and the active layer with the selection's surviving members when the active layer still exists, as Swift's; history starts over. An open (the controller's own, which the workspace's tabs use) and a save remember the digest, then watch the package; a save marks saving from before its worker starts until the digest of what it wrote is remembered, or, when it fails, until its alert is answered (Swift's defer), so its own events pass; a new canvas and a close stop the watch. Swift's decoding from bytes in memory (cba6efc) has no twin: QImageReader decodes a PNG into memory the image owns, never mapped from the file, so a later save that replaces the file cannot reach an image kept for undo. Equivalent mutants: the try count's reset on a new event (a missing folder's creation is itself an event on the package, which re-arms), and the reload's two log lines. Tests: ExternalChangeTests (Swift's six; the revert question both ways, Keep Mine remembering, a background tab asking when brought forward, a busy project backing off then reloading, new canvas and close stopping the watch and its recheck, one check at a time, a check ending without a project or during our save), ExternalChangeGateTests (a failed save's guard, a managing workspace, the back-off's intervals, a load landing elsewhere, the reload's whole view and last signal, an unreadable manifest, Save As and open moving the watch, a second change while the question waits), ExternalChangeFixtures.h, ProjectDigestRaceTests (a binary of its own: readdir and readdir64 take libc's place and remove an image as its folder's listing ends, so the digest's throw is ordered, not raced), ProjectWatcherTests (one report a burst, the quiet time from the last event, twenty re-arm tries, each watch speaking alone, an exchange keeping the old package, precise timing, re-arming and giving up, the digest's exact input and order, an unreadable manifest).
  • Editing while a project saves (12.13b, Swift 1.3: 940e188): a save holds the tools only while it captures the document (prepareSave, Swift's: the revision, the snapshot, and the Save panel when there is no path yet or for Save As), then reserves the writer and only then frees the project, so no save asked in that signal slips in beside it, and writes in the background; a capture finds no document when Undo took it while the save waited (Swift's guard let snapshot), and the save answers false; DocumentHistory::currentRevision and markSaved(QUuid) twin Swift's, so only the captured version counts as saved: edits made while it writes stay modified, and undoing back to it is unmodified. Swift's writing task is m_writing with a list of waiters: finishWriting runs its callback at once when nothing writes, else when the write lands, each waiter posted to the event loop and passed through finishWriting again, so one writer runs at a time where Swift's store actor serialises the disk; save waits there before begin, so a second save writes what changed since, and confirmReplacement waits there first, so a close, a quit, a new canvas or an open never cuts a file short and asks only once the write is whole; an open also waits before its first read, and so does an external reload (12.13a's reloadFromDisk), since the twin's store is no actor and could read a package mid-save: a Revert answered while our save writes loads what we wrote. A digest that comes back while our save writes is looked at again once the write lands (pending, through finishWriting): after a save it matches what we wrote, after a failed one the change is taken up; Keep Mine remembers its digest only when no save landed while the question was up. A save queued behind a quit that already holds the tools is refused, as Swift's begin refuses it. A save that fails frees its waiters after its alert is answered. The refusal of a completed Save As name that exists sits in prepareSave, beside the panel. Swift has no tests for this commit: ProjectSaveWhileEditingTests pins it (edits while writing, a second save, a new canvas waiting, the panel alone holding the tools, a failed write, the last signal), and, over tests/SaveHooks.h (fsync slowed and counting overlapping writes, renameat2 held at a package's swap, readdir and readdir64 held at one folder, the store's loads counted from its log, every hold let go on cleanup), ProjectSaveOrderTests (a reopen reading nothing while the write is held, a Revert mid-save, the guard through the save's digest, Canvas Size and Image Size each committed before and after the held write lands, a digest out as our save begins, a failed save keeping an external change in line, a second save waiting through the first save's digest, Keep Mine after a save) and ProjectSaveQueueTests ( three saves one at a time, a save asked as the tools free, a quit waiting, a document undone away, a failed write holding its waiters, a reopen, a tab closing, a controller gone mid-write, an export beside a write).
  • The guide for agents (12.13c, Swift 1.3: c317306): docs/writing-comp-files.md twins Swift's guide and docs/project-format.md Swift's format page, both adapted to Linux (a project is a folder, the example lives under ~/Pictures, the save swaps folders with renameat2, a replaced project folder is noticed too, sources named by the twin's files). Swift's top-level AGENTS.md for agents becomes the section at the head of this file. README gains Swift's lines (scrubbing, saving while editing, projects changed on disk, working with AI agents). The format page's claims follow the twin, where Swift's page is out of date: the pixel budgets scale with memory, the save's exchange has a two-step fallback, only maskPlacement needs a mask, the common adjustment fields are required and later kinds default to their own values, folders pass adjustments through, text becomes pixels after Canvas Size, Image Size and Crop, and a missing face may fall to another family. WritingProjectsGuideTests reads the guide itself: the minimal manifest loads with the fields it shows (visibility, opacity, blend, placement, the image named after its layer), the Curves layer's points are pinned and its rendered pixel is the background through those curves, removing any common adjustment field refuses the project, a folder passes an Invert through, hsvSettings wins over the legacy Hue/Saturation fields (the guide says where to edit a saved one), and the guide's lists of blend modes, adjustment kinds and samplings equal the twin's rawValues. The guide asks for 8-bit RGBA layers and accepts what the store accepts: RGB, grayscale and lower-depth grayscale layers, never 16 bits. ExternalChangeTests::theGuidesSameSizeRecipeReloads runs the same-size recipe against a watched project.
  • Quitting while text is edited (12.14a, Swift 1.3.1: a18d86c, afd622e, 4a4cf85, 221d6cc): ProjectWorkspace::confirmQuit first applies every tab's open text in quit order (finishTextEditing, Swift's), so closing the window (the twin's quit) no longer stops at canSwitch, which an open draft refuses; text that cannot be applied (a busy project) stops the quit with the draft kept. Swift's applicationShouldTerminate guard has no twin: closing the window reaches confirmQuit directly. Swift's cursor commits (the arrow restored off the canvas, the I-beam kept across it while text is open) need no code here: a Qt widget owns its cursor, so leaving the canvas shows the next widget's, and the canvas already shows the Type tool's I-beam away from the box. Tests: QuitWhileEditingTextTests (Swift's two through the real window's close: the text applied as one New Text Layer step before the question, Cancel keeping the window, then Don't Save; every tab in quit order with its own content and its undo; a refused draft in the middle of three tabs stopping the quit where it stands, the tab before applied, the tab after untouched, nothing asked; edited existing text as one Edit Text step on the same layer; an unchanged existing draft and a blank new one closing with no step and no question), InlineTextHandleTests::theIBeamHoldsAcrossTheCanvas (the I-beam away from the box, after a resize drag, no application-wide cursor on leaving, and neither the window nor a sibling taking the I-beam).
  • Version 1.3.2 (12.14b): IO/RecentProjects twins Swift's class as a QObject singleton with one signal, changed(). Swift mirrors the list macOS keeps (NSDocumentController); Qt has no API for the desktop's list, so the twin keeps its own in QSettings under recentProjects, ten places at most, newest first, each resolved as samePlace resolves it (so a link and its target are one entry); refresh drops places that no longer exist and signals only on a change, and runs on activation, where a GUI application exists (a GUI-less run has none). The controller notes a place after a save lands and after an open installs, never after a failure; open refuses a place that no longer exists before any read, refreshing the list, with the alert "Couldn’t open the project" naming it in Cocoa's words ("The file “Name.comp” couldn’t be opened because there is no such file."). The File menu's Open Recent follows Open Project…: each place titled without its suffix (ProjectTab::nameWithoutSuffix), opening it through the front tab's controller (so the workspace, as Swift's projects.open), then a separator and Clear Menu (clearRecent), resting while the list is empty; the whole submenu (openRecent) rests while the controller cannot start. Swift's showEditor has no twin (the window is the editor). The Image Size sheet keeps m_lastResolution, the last usable resolution: a zero or negative entry leaves the print size alone, a new valid one scales the pixels from it, and a width or height typed in Inches or Centimeters meanwhile is ignored (555417e); a resolution set by a print size without resampling goes through setResolution, as Swift's onChange sees every write. Camera Raw's Color Grading sits under Color and opens with Light and Color (7e9afbe): the Section enum, titles, eyes and adjusts follow the new order. Swift's d62a132 makes every window dark whatever macOS is set to; the twin's windows, alerts included, already take the built-in dark or the Omarchy theme through the application palette, and follow Omarchy by design (Linux-only behaviour); file panels are the desktop's own, which on Omarchy follow the same theme, so nothing changes. A reload after a save over the opened place notes the place again, since another note may land between. The alert's words are plain text (qt_msgbox_informativelabel), so a name is never read as markup, and a menu title doubles its ampersands. Tests: OpenRecentTests (the stored list at start, activation checking again, nothing noted for a cancelled replacement, a cancelled Save As or a failed save, a save noted once it lands, the reload's note after another, the submenu's title and place, a literal ampersand and markup, opens and saves noted newest first, a failed open noting nothing, ten kept and a link counted once, moved projects dropped with one signal, the menu listing, opening, clearing and resting, a deleted project named), RecentProjectsCoreTests (the list without a GUI application, and across launches: note, read and clear each in a fresh process of the binary), ImageSizeSheetTests::aNewResolutionKeepsThePrintSize and aDocumentsOwnResolutionIsTheLastUsable (a 300-ppi document, zero, negative and infinite entries in both physical units with and without resampling, Pixels and Percent edits allowed meanwhile, positive resolutions past Resize's range remembered), CameraRawPanelTests::eachGroupOpensAndItsEyeHidesItFromThePreview and eachEyeHidesItsOwnGroup, the alert's palette in OmarchyThemeTests, the File menu's count and Open Recent's gate (busy, the new-canvas sheet, importing, the importer, the workspace's panel) in CompositorMenusTests.
  • Coloured filter sliders (12.14c, Swift 1.3.2: 6b709b4, c8ed9ba, 0c969af): CameraRawSliderTrack gains Swift's opposing (two colours, from and to) and spectrum (thirteen stops every 30 degrees, the hue centred), and colors returns a list spaced evenly along the track, where Swift returns an array. CameraRawSlider::reshape is Swift's updateNSView: new bounds and track, the value shown again unless a drag holds the knob; track() reads the track. The filter sheet's control takes an optional track (FilterSheet+Controls.cpp, since FilterSheet.cpp would pass 500 lines): with one the row is a CameraRawSlider over the same thousand steps, linear (a logarithmic coloured row is a logic_error), helped "