312 KiB
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.
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'sCompositor/Rendering/*.cand compiled in place. packaging/: the desktop entry, MIME type, icons (Compositor's app icon) and the ArchPKGBUILD;flake.nixat the root for NixOS.scripts/install/: one install script per distribution family, each fetching the latest release's package..github/workflows/release.yml: av*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:
ImportedImageexposes pixels throughimage(), size throughsize(), identity throughidentity(). CoreGraphics backs a painted layer'sCGImagewith 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 (
===onCGImage) isImageIdentity: theRasterSnapshotpointer for painted assets,QImage::cacheKey()otherwise. - Layer pixels are
QImage::Format_RGBA8888_Premultiplied; masks areQImage::Format_Grayscale8.BrushRaster::contextmakes both. - Contexts are top-left already, so the CoreGraphics y-flips vanish.
BrushRaster::drawhas nomaskflag: both CoreGraphics paths are a plain copy in Qt. It takes an optional source part instead ofCGImage.cropping(to:), which Qt cannot do without a copy. BrushRaster::visibleRectstands in forboundingBoxOfClipPath.- Images are drawn into explicit target rects. The positional
drawImage(x, y, image)scales by the source's device pixel ratio; CoreGraphics never does. DownsampleCachehalves with its own Lanczos-3 filter (vImage has no Linux twin), threaded with QtConcurrent. Swift's label-overloadedimage(_:drawnAt:)andimage(_:level:)areimageDrawnAtandimageAtLevel. Its constructor takes a pixel budget so eviction can be tested; the app usesshared().LayerRenderer::drawtakes Swift's labelled arguments as anOptionsstruct. 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-filledAlpha8surface 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 beforesave().LayerRendererneeds a painter on aQImage.- CoreGraphics multiplies nested mask clips inside the context; QPainter has no soft clip.
FolderMaskClip::applymultiplies a folder mask into device-sizedAlpha8coverage,FolderMaskClip::drawhands each layer the product of its folders' coverage (memoized per folder), and draw routines take it asLayerRenderer::Options::clip. TiledLayerRendererkeeps 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-openQRegioncomputed 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 onceAntialiasingis 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 insideLayerRenderer::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::compositetakes an optionalveilcallback that repaints the mask coverage after the mask is drawn (coverage starts full when the layer has no mask).drawStrokedraws 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.drawMaskStrokedraws 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::drawBrushPreviewreplaces Swift's even-odd clips withCompositionMode_Sourcecopies into the aside surface, hard-edged (compositeruns 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. ItsBrushPreviewstruct 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 throwsstd::logic_erroron an empty pixel grid, where Swift would divide by zero.LiveMaskRendererhas nobounds. 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 whatOptions::cliptakes.drawOwnreceives that coverage, and an outer clip (folder masks) multiplies in; a clipping stack composites its group one to one throughLayerRenderer::composite, under the painter's own opacity as in Swift;drawsaves and restores the painter arounddrawOwn, 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 onomaphoto.renderingwhere Swift skips in silence.AdjustmentSurface::drawallocates 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, asCGContext.drawinherits 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).ImageExporteris a namespace of functions. Swift's actor only serialises calls; the twin keeps no state, and callers run it off the UI thread. Files areQStringpaths. PNG and JPEG go throughQImageWriterwith an sRGB tag and the resolution as dots per metre; the JPEG preview is read back withQImageReader, scaled to 1000 pixels at most.writereplaces the file throughQSaveFileand throwsstd::runtime_errorwith the system's message, where Swift rethrows the Cocoa error. Exports log onomaphoto.io.ProjectStoreis a namespace too. Swift's synthesizedCodableis written by hand inIO/ProjectStore+Manifest.cpp: the same keys, the same refusals (wrong types, missing required keys, non-canonical UUIDs), unknown keys ignored,CGPointandCGSizeas 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 plainrename. 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.oldneighbour after two moves) and a critical log onomaphoto.ionames that path.ProjectReplaceTestsdefinesrename,renameat2,fsyncandreadin the test executable, where they take libc's place, to order and fail each call. There is noNSFileCoordinator. A missing manifest isProjectError(invalid)and a missing assetmissingImage, where Swift passes on a Cocoa error; a failed write throwsstd::runtime_errorwith the system's message.QFilereports no error when a read fails after partial data, so the manifest's byte count is checked against the file's size. The manifest'sversion,widthandheightareqint64, Swift'sInt, read from Qt's exact integer (QJsonValue::toVariantasLongLong), 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.223372036854775808e18read as the minimum where Swift refuses them, and9007199254740993.0loses 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.ProjectTestspins 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), everyUInt32of which is valid, turns that into a project Swift opens and the twin refuses;aSeedReadsWhatQtsParserReadspins it. - Qt refuses image decodes over 128 MB (
QImageReader::allocationLimit).ImageImporter::liftAllocationLimitlifts that limit for the store and the importer: their own pixel budgets are checked from the file headers before any pixel is read. ImageImporteris a namespace; the Swift actor only serialises calls and caches aCIContext. The type comes from the file's bytes, asCGImageSourceGetTypereads it:QImageReader::setDecideFormatFromContentfor JPEG, PNG and TIFF (TIFF needsqt6-image-formats-plugins), libheif'sheif_get_file_mime_typefor HEIC (image/heic: HEVC in HEIF). Other ISO media (AVIF, plain HEIF, video) and every other format Qt recognises areunsupported; bytes nothing recognises areunreadable. 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 thenclxbox, 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'sQColorSpaceconstructor 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 areunreadable, each with its reason in the log: no HEIC imports with colours the importer knows to be wrong.heif_initloads the decoder plugins (Ubuntu ships HEVC decoding aslibheif-plugin-libde265); a missing decoder isunreadable, 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'ssetAutoTransformdrops the colour space. Untagged pixels count as sRGB, as CoreImage counts them. Qt 6.4 reads no orientation from a PNG'seXIfchunk (probed:supportsOption(ImageTransformation)is false), so such a PNG imports as stored; JPEG, TIFF and HEIC turn upright. The result isRGBA8888_Premultiplied, tagged sRGB. A thumbnail's long side is 96 and its short side rounds up, as Swift'sextent.integraldoes. The HEIC fixtures intests/fixturesare committed;scripts/heic-fixtures.shmakes them and stops at the first failure, because the dev image carries no HEVC encoder. The tests patch the losslessp3-nclx.heicin 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 maskdrawwould 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).DrawCoverageTestsholds 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 alogic_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 placementdrawtakes,radians()included, so a wild angle turns both alike. A mask that is not gray is alogic_error; a white image that cannot be allocated isExportError(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.
ImageResizerandCanvasResizerare namespaces.Task.checkCancellation()in the Swift resizer has no twin: no caller cancels a resize. Each turns astd::bad_allocintoExportError(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 byLayerTransform::isValidbefore 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 holdqint64, Swift'sInt, like the manifest.CanvasSizeDraftkeeps plain members where Swift haslet, so the struct stays assignable.EditorSessionis aQObject. 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, asCanvasView.synchronizeDisplaydoes.beginEditemits asendEditdoes, because an open transaction closescanUndoandcanRedo.fitandzoomemit only when the view changed, so a zoom that is no number stays silent. The canvas editsviewportin place and then callsnotify(). A Swift property with adidSetis a private member with a getter and a setter (activeLayerID,isProjectBusy,showsNewDocument,showsImporter,isImporting,importError,renamingLayerID);document,tool,collapsedGroupIDsandselectedLayerIDsare read through getters and change only through session methods;historyandviewportare public members, as Swift's tests use them.activeLayer()returns a copy, as Swift's computed property does.EditorSessionSignalTestsholds 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.swiftruns to 741 lines, so the twin splits:EditorSession+Model.h/.cppholdImageLayer,CanvasDocumentandNavigationTool(their own header also breaks the include cycle withDocumentHistory, which a member by value needs complete),EditorSession.h/.cppthe core,EditorSession+Layers.cppthe layer operations,EditorSession+Transform.cppthe transform edits,EditorSession+Imports.cppthe import queue and the busy gates;EditorSession+Projects.cpptwins 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 itsName+Session.h: theLayerGroupsextension (folders, the multi-selection, grouping, placing,layerRows) inLayerGroups.cpp,finishDeletingLayer(s)inLiveLayerMask.cpp.groupSelectedLayersfollows 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: thenilthat 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).placeLayerdrops Swift'starget != idfor the same reason: the target is sought after the layer is taken out, so a layer is never found above itself.canPlaceLayeris tested by itself as well as throughplaceLayer, whose validation would hide its terms; the panel asks it before a drop.selectLayersandextendSelectionfall back on any selected layer, as Swift'sSet.firstdoes: tests never depend on which. A session's document always passesLayerHierarchy::validate(every mutator checks before it commits), so everyparentIDnames a folder of the document;groupSelectedLayersreads parents throughstd::map::at, which throws if that ever breaks.- Transform edits:
TransformEditholds what exists so far (layerID,draft,group);persistent,corners,maskandfloatingarrive with the items that read them (TASKS.md, "Session hooks owed"). A draft changes only throughpreviewTransform, which refuses an invalid value, and begins as a valid transform or box, so it is valid by construction.commitTransformtherefore gives each layer whatpendingTransformgives 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'sedit.draft.isValidandfirstIndexguards 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.canTransformandgroupTransformMembersdrop Swift'sisGroupterms (a folder never holds pixels,LayerHierarchy::validatesees to it, soassetsays the same), andbeginTransformdropstransformEdit == nil(canTransformreads it throughcanEditLayers).groupTransformBoxkeepsmax(1, …): rounding leaves a one-pixel layer at origin 1.3 a box under one pixel, and a test holds that.DistortWarp::cornerscame forward from 6.2 for the group box. - Swift's
asyncsession 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 itswhileloop: no waiter runs inside the setter, and the first may take the project before the second looks.importImagestakesQList<QUrl>(a URL that is no local file isunsupported, Swift'sisFileURLguard), decodes one file at a time throughQtConcurrent::runand aQFutureWatcher(the worker catchesImageImportErrorand 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 oneImport Imagestransaction. 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, afterisImportingfell 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.showsBusyis a single-shotQTimerofbusyIndicatorDelay(250 ms), aQt::PreciseTimerbecause 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;createDocumentclears its two flags and then resumes waiting file requests once, where Swift's twodidSets each try. The session therefore needs an event loop: every test that makes one usesQTEST_GUILESS_MAIN. - Appearance:
allLayerBlendModesstands for Swift'sLayerBlendMode.allCases, the order of the blend menu. Swift'srefreshCanvasPreviewcallback has no twin:changed()reaches the canvas, andpreviewBlendModeemits it only when the preview changes, so hovering over one entry redraws once.blendPreviewis a small struct where Swift has a tuple.opacityEditLayerIDis a private member: only the session reads it; an open opacity drag shows outside as an open transaction (canUndois false). Inside a dragsetLayerOpacityopens 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
projectURLisprojectPath, aQString, since files are paths here; it gets a setter whenProjectControllerneeds 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)andCanvasDocument(snapshot), so both places agree by construction; shapes and text set them apart:installProjectloads both afterCanvasDocument(snapshot),applyDocumentSizedoes not, as Swift's rebuild leaves them out.applyImageSizeandapplyDocumentSizelive inIO/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::adopttakes 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-builtProjectSnapshot, where Swift's tests write intosession.document. - Masks in the session (
LayerMask.cpp, after the model):isMaskSelectedhas a getter only, since every Swift write of it sits in a session extension.TransformEditgainsmask.commitTransformhands a mask-alone edit tocommitMaskTransform, which writes the placement inside one transaction and lets the history drop a change that changes nothing, where Swift compares placements first. IncopyMaskandtoggleMaskLinkSwift'sfinishOpacityEdit()aftercommitTransform()is left out:commitTransformhas already finished the drag.tests/SessionFixtures.hholdsrewrite(session, change), which installs a changed snapshot where Swift's tests write a mask, a flip or a sampling intosession.document; it resets the history as any install does, so steps are counted from there.tests/SessionRecord.hholdsdescribed(), the text of everything an observer can read, shared by the signal test files. - Live masks in the session (
LiveLayerMask.cpp):adoptClippingandreleaseDetachedClippingare static members, as Swift'sstatic funcs.removeLiveMaskandreleaseDetachedClippingwalk the layers once per parent in list order, where Swift builds sibling arrays; the runs they free are the same.LiveMaskGraph::validatealready refuses a link to oneself, a folder at either end and a missing source, socanLinkMaskasks only for editable layers, an existing target and a valid graph; Swift's own checks of those cases, both folder checks oftoggleClippingMaskandcanToggleClippingMask(the layer's own and its lower sibling's: the store validates the graph on load andcanLinkMaskon every link, so no folder is ever clipped and none becomes a base), and the folder case of the run's base inreleaseDetachedClipping(nothing is ever clipped to a folder) are dead and left out; both toggle functions take the base from one helper,clippingBase.adoptClippingkeeps its folder check: it writes the link without asking the graph, andLiveMaskEditTestscalls it directly, becauseplaceLayer's release hides the side of the rule that looks below.deleteWithLiveMaskChoiceshows aQMessageBoxfrom the session, because Swift's session shows anNSAlertthere: three buttons with Swift's texts (accept, reject and destructive roles), andLiveMaskDeleteTestsdrives the real alert from a zero timer underQTEST_MAIN. The bake runs throughQtConcurrent::runand aQFutureWatcherwith the project busy; layers are deleted first and the project freed last, as Swift'sdeferdoes; a failure setsbrushError(a getter and a setter, arriving here because this is its first writer) and logs a warning.LiveMaskBaker::bakedraws 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 alogic_erroronce the target and its pixels are found, where Swift'sDictionary(uniqueKeysWithValues:)traps. The bake's worker catchesExportErroralone, as the import's worker catchesImageImportError: anything else is a bug and is left to crash.releaseDetachedClippingkeys its runs by the optional parent itself, because all zeros is a valid folder id.PixelAdjust::thumbnailcame forward from 9.2 for the baker. Over the renderer's pixel budget a bake draws nothing, on macOS too. ProjectControlleris aQObject. Itsasynccalls take a completion callback, posted to the event loop like the session's; steps inside continue inline.newCanvasandclosetake none: no caller awaits them. The store runs throughQtConcurrent::runwith oneQFutureWatchera call, a child of the controller, so an answer dies with it; the worker catchesstd::runtime_error(both of the store's error types) and anything else crashes. Panels areQFileDialogs and alertsQMessageBoxes shown withopen()overwindow(aQPointer<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 inDirectorymode and takes one project (noQFileDialogmode takes several folders, as Swift's workspace panel takes several packages); the save panel is a file panel with thecompsuffix, 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 asselectedFiles().value(0). A chosen name that does not end incomp(in any case) gets.compappended, whereNSSavePanelenforces the project type:setDefaultSuffixonly 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.opentrims the slashes at the end of its path, supplied or chosen in the panel: a shell completes a folder's name with a slash, andQFileInfofinds no name behind one, where Swift'slastPathComponentdoes. It trims only the end:QDir::cleanPathwould also collapselink/.., which names another folder oncelinkis a symbolic link. Callbacks that the session keeps (waitForFileRequest,importImages) hold the controller through aQPointer, 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'ssession.projectURL = …issetProjectPath, which notifies;markSaved()needs no signal of its own, because every path from a save goes on to free the project, and that notifies.samePlacecomparescanonicalFilePath()s where Swift comparesresolvingSymlinksInPath(). There are no recent documents (Qt has no API for the desktop's list) and no security scopes.receivekeeps Swift's queue: requests drain one at a time from the event loop, each afterwaitForFileRequest; a project is known by its suffix in any case, a trailing slash aside.tests/DialogDesk.hanswers the real dialogs from a timer: it types into a panel's name field (selectFiledoes 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 savedUntitled.compthere and skewed the runs after it).ProjectWorkspaceis aQObjectwith one signal,changed(), as the session has: it mutates itself, so the rule for@Observableclasses that only the session writes does not cover it.tabs,selectedIDandisManagingare read through getters. AProjectTabholds its session and controller by value, the session first, and lives in astd::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'sawaitdoes, 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 aQPointer. Swift'sasynccalls take completion callbacks posted to the event loop;closetakes one too, because its tests await it. The open panel takes one project (see the controller) and is titledOpen Projectlike the controller's, where Swift's bareNSOpenPanelsaysOpen: Qt's default for a folder panel isFind Directory. Managing ends last in every flow, after the sessions are free again, so that the workspace's last signal seescanSwitchas the call leaves it. A title cuts the suffix as Swift'sdeletingPathExtensiondoes (nameWithoutSuffix: a name's first or last dot cuts nothing, so.compstays whole), wherecompleteBaseNamewould leave nothing.samePlacefalls back on the absolute path (absoluteFilePath, which also cleans it) when a place no longer resolves, asresolvingSymlinksInPathdoes, 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), soloadProjectnames it from the path as given.receivelooks again every 30 ms while the workspace cannot switch, as Swift sleeps;Task.isCancelledhas no twin.receiveProviderstakes the drag'sQMimeData: 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 toImageFileDropsince 10.4, which reads nothing there); files and pictures arrive with 10.4.copyLayerbakes 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::isProjectandsamePlaceare public statics so that both classes read paths alike. The anchor's?? centrefallback is dead (the copied layer is always among the copies) and is a checked read instead.CanvasViewtwinsEditorCanvas.swift, split asEditorCanvas.cpp(the display) andEditorCanvas+Navigation.cpp(events, cursors, focus; keys inEditorCanvas+Keys.cppsince 12.2b). It follows Swift'supdateNSView:ContentViewcallsconsumeFocusRequestandsynchronizeDisplayon everychanged(), and a canvas alone follows nothing, as Swift's alone observes nothing.paintEventclips to the dirty rect (the region's union, asdraw(_:)receives it) and paints it throughAdjustmentSurface::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.DisplayStateholds what Swift's holds (brushRevisioncame with 8.2, adjustments with 9.5, effects with 9.6c, the crop union inrenderBoundswith 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'sBase, which the built-in dark sets to Swift's 0.105 gray and a light Omarchy theme to its lightdarker_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::ZoomNativeGestureismagnify. Geometry reaches the viewport from the event loop (syncGeometryis posted, as Swift defers past SwiftUI's layout) and notifies the session;QWindow::screenChangedstands in forviewDidChangeBackingProperties. A shown canvas takes the keys when a document exists and no sheet, importer or modal dialog holds them;EditorSession::requestCanvasFocuscounts Swift'scanvasFocusRequest,NavigationToolHeaderasks on Return and Escape, andContentViewposts one when a document appears (Swift'sonChange). 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 anyNSTextfirst 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::visibleRectandAdjustmentSurface::drawwork on widget painters as on images: a widget'sQPainter::device()measures itself in logical pixels where an image's measures device pixels,combinedTransform()carries the device pixel ratio, anddeviceTransform()also the widget's offset in its window's backing store, whose image is the engine'spaintDevice().HighDpiCanvasTestsholds this at ratio 2 through amainof its own that setsQT_SCALE_FACTOR.EditorSession::drawLiveCompositelives inLiveLayerMask.cpp, as in Swift, and draws the document as the export does, in document pixels, through the displayed transforms, placements and blend modes;LayerMaskSessionTestscompares it with the export on every render.ContentViewis one project's editor area, aQWidgetthat compares what it shows with what the session holds on everychanged(); the window around it isProjectWorkspaceView(4.3c), which owns the toolbar that SwiftUI letsContentViewdeclare.ToolHeaderBarcarriesQ_OBJECT: Qt 6.11'sfindChildrefuses a class without one at compile time. Every tool has a bar ofToolHeaderStyle::height, so the canvas never jumps: a tool whose bar has not arrived yet shows an emptyToolHeaderBar, the idle and Eyedropper bars are titles alone, Hand and Zoom share oneNavigationToolHeader, 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'sif document == nilmakes a fresh view, so its fields and its clipboard suggestion start over. A rail button is aQToolButtonthat toggles itself on a click before the session answers, so the click's slot sets every button again from the session, which a refusedselectToolleaves silent.NewCanvasSheettakes Return and Enter from anywhere on the sheet through widget shortcuts, as SwiftUI's.defaultActiondoes:setDefaultalone reaches only aQDialog. Fonts are set in pixels, Swift's points on a display of one:setPointSizewould grow them by the screen's DPI. Swift's.secondarytext is the palette'sPlaceholderText, its orange warning the palette'sBrightText, which the theme of 4.5 fills; dividers areMid. 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.NavigationToolHeaderapplies 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 throughtoDouble, which the> 0test refuses, so nookflag is read;infreads as a number and the viewport refuses it.NewCanvasSheet::clipboardDimensionsreads a copied picture's size from its header throughQImageReader(no pixel is decoded), PNG before TIFF as in Swift, sides swapped for a stored quarter turn. The importer is aQFileDialogfor existing files, and the two alerts areQMessageBoxes 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.LayersPaneltwinsLayersPanel.swift: the heading with its count,LayerAppearanceControls, the rows or the empty words, and the footer'sQToolButtons (new layer, folder,LayerMaskMenu, the effects menu since 9.6d, the adjustment menu since 9.5d, delete). Its width isContentView'slayersPanelWidth, kept inQSettingsunder Swift'sAppStoragekey; a stored width outsideminimumWidth…maximumWidthis the default, logged.PanelResizeEdgelives inContentView.cppas in Swift: an 8-point grip with the split cursor over aMidhairline, dragged left to widen, whole points, within range. Tests enableQStandardPaths::setTestModeEnabled, which sendsQSettingsto~/.qttest. The delete button is Swift'sdeleteLayerOrMask(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).NativeLayerListtwins theNSTableViewwith aQScrollAreaofLayerCells, one a row, made anew only when the ids change (a selection or rename reconfigures the cells in place, Swift's "no reload" rule;nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentitycompares 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 aThumbnailKey(asset identity, transform, canvas size) so a picture is redrawn only when what it shows changes.EyeSwipeButtonandLayerThumbnailButtonareQToolButtons 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 asautoscroll(with:)does; Swift tracks the drag insidemouseDownwith adefer, 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'smouseDownorder: Alt on a mask chooses the mask on a release without a drag, Shift toggling it asselectMaskdoes (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'smouseDownruns twice), and Alt's on the row's strip clips again; a plain one is left to the row, which renames as Swift'srenameClickedLayerdoes for a pixel layer (text opens since 8.9a, an adjustment's settings since 9.5d).QTest::mouseDClickon 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 isclickRow(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'sQLineEdit(Return keeps, Escape cancels, leaving keeps) and the session'srenamingLayerIDis cleared before the rename, because the rename's own signal would reopen the editor; then the list takes focus, as Swift'swindow.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: anNSMenunever ends editing. The name and size labels areQt::PlainText, so a name is never read as markup. The context menu is the cell'sactions(), named as Swift's menu items. Hidden rows fade through aQGraphicsOpacityEffect. 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'supdate, the panel's and the mask button's icons; a folder's icon keeps its ink in itsThumbnailKey. The canvas's application event filter already takes Shift-+ and Shift-− from the list;BlendShortcutTestsholds both and the text-field exception.- Drags in the list (
NativeLayerList+Drag.cpp): a row pressed and moved paststartDragDistancestarts aQDragcarrying the row's id, or its whole selection's, one id a line underProjectWorkspace::layerType(Swift writes one pasteboard item a row); Alt at the press offersCopyActionalone underCanvasView::duplicateCursor, as the table's source mask does, and the drop then duplicates.dropTargetis Swift'svalidateDrop: a folder's middle half takes the drop in, the rest lands above the nearer row edge or below every row, every row must passcanPlaceLayer(a copied folder carries its contents since 12.2a);acceptDropisacceptDrop, the place worked out once, the order reversed into a folder, the moved rows kept selected, oneMove Layer(s)orDuplicate Layer(s)step. Swift refuses a drag from another table by its source; the twin's drag carries its list's token undersourceTypeand a drop takes only its own (Qt's synthetic drop events carry nosource(), 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'smouseDownfalls 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 paststartDragDistancestarts 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 aQWindowis 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 setCopyActionon 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.LayerColumnpaints 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 undermaskType, read back throughProjectWorkspace::layerID(a payload that is no id names no mask, an all-zero id included), and lands on the row under the pointer throughcanCopyMask.QDrag::execneeds the platform's drag loop, whichoffscreenhas not:LayerDragGestureTestsdefines bothQDrag::execoverloads in the test executable, where they take libQt6Gui's place (asProjectReplaceTestsdoes with libc), record what the drag offered and returnIgnoreAction, so presses, thresholds and actions are tested to the call;LayerDragTestsholds the drops andLayerDragFixtures.hwhat both share. The eye and thumbnail buttons live inNativeLayerList+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 wherecanToggleClippingMasksays 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'sflagsChangedmonitor and tracking area; the list keeps the last hover point, so a modifier change is judged where the pointer sits (QCursor::posis no witness offscreen), and a hover's own modifiers are the event's. EditorSession::duplicateActiveLayerandduplicateLayer(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, oneDuplicate Layerstep; 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;duplicateLayerselects, duplicates and places the copy inside one transaction, refusing a placecanPlaceLayerrefuses (which readscanEditLayers, Swift's first term) and answering false when the limit made no copy.LayerMerge.cpp,LayerFlip.cppandFilters.cpptwin the Swift files of those names (Filters.cppholdsPixelFilter::trimmedalone until 9.x).trimmedreturns aTrimmedstruct where Swift returns a tuple, and reads the alpha bounds straight from a premultipliedRGBA8888image, drawing any other format into one first, where Swift always draws into a context.mergePlanis a privateMergePlanstruct.mergeLayersrenders throughdrawLiveCompositeinto a document-sized context and takes the trimmed image's own size throughImageLayer(asset, origin), where Swift also assignstrimmed.transform: at 1:1 the two are equal by construction. Its insertion index counts the kept layers before the anchor, so Swift's?? layers.countand 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 onomaphoto.rendering, as is a context that cannot be allocated (Swift returns in silence).flipLayersreads the group box and the active layer throughvalue(), sincecanTransformhas proved both, and drops Swift'sids.isEmptyguard for the same reason;commitTransformhas already runfinishOpacityEdit, so Swift's second call inmergeLayersandflipLayersis left out.LayerTransform::mirroredis declared inLayerTransform.hand defined inLayerFlip.cpp, as Swift's extension.beginDuplicateTransformkeeps a privateTransformDuplicate; a selected folder carries its selected contents, which are no targets of their own, and since the layer limit can refuse a copy, Swift'scopies.isEmptybranch (the step closed, the selection back) and itscopy != idtest are kept (12.2a); a cancel removes each copy with its contents and their collapsed marks.commitTransformends the duplicate after its own transaction (endDuplicateTransform, Swift'sdefer) on the layer path only:selectLayershas just made a copy active, so no mask is selected and the mask path never carries one.cancelTransformremoves the copies, restores the selection and ends it. The Layer menu's Duplicate Layer entry callsduplicateActiveLayer, what Swift'slayerViaCopydoes without a selection (7.4 brings the selection); Merge showsmergeTitleand Flip Layer followscanTransform.- The Move tool on the canvas (6.1):
TransformOverlayis 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, andsynchronizeDisplayrepaints only the union of the old and the newdrawnRect(a guide spans the canvas) when the geometry orsnapGuideschanged, leaving its return value to the pixel state.TransformOverlayGeometrykeeps Swift's handles, rotation grip andhit;showsRotationand the corner constructor arrive with 6.2.resizeCursormaps Swift's frame positions onto Qt's four resize shapes (right, bottom right, bottom, top right).EditorCanvas+Transform.cppholds the drags (beginTransformDrag,dragTransform,endTransformDrag,cancelTransformDrag, Swift'sresignFirstResponder),transformPressLayerandtransformCursor; Ctrl stands for Cmd in picking, Ctrl+Shift extends the selection. The drag's cursor is kept inm_dragCursoruntil the release and outranks Space, where Swift disables the window's cursor rects; hover reachesupdateCursorthrough 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) androtationCursor(two arrows round a ring, for the SF symbol) are drawn as paths; the distort cursor arrives with 6.2.SnapGuidesis a struct where Swift has a tuple, a public member likeviewport, unannounced;commitTransformandcancelTransformclear it.transformSnapTargetsandsnappedMovelive inDocument/Crop.cpp, Swift's file.TransformEdit.persistentsits last, so the positional initialisers keep their order; a drag's edit and a duplicate's are not persistent.transformAutoSelect,showsTransformControlsandlocksTransformRatioare 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 itsdrawtests. - Distort (6.2):
DistortWarpkeeps Swift's tests of usability and convexity,homography(a projectiveQTransformwhere Swift returns a closure, Qt dividing by the third row as Swift byw),warp,warpTrimmed,carriedandwarpMask. Core Image's perspective filter isQTransform::quadToQuadon the image's pixel rect and adrawImageunder 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, andQRegion's integer polygons round the vertices), copied throughBrushRaster::drawwithout interpolation, as Swift's. IndistortPreviewa mask warp that throws is caught on its own, as Swift'stry?: 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. ItsWarpedandTrimmedstructs stand for Swift's tuples;mapPatharrived with 8.2b anddistortedEffectsarrived with 9.6c. Swift'sDistortTestsagree 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.cornerssits last, afterpersistent. The session'sdistortPreviewandmaskDistortPreviewareconstwith mutable caches (DistortPreviewCacheby layer,MaskDistortPreviewCache), keyed byImageIdentitywhere Swift comparesCGImageidentity, because the canvas reads them from a const session; a warp that throws leaves a cached empty result, as Swift'stry?.commitDistortcatchesProjectErrorandExportErrorintobrushErroras Swift catches its errors;commitTransformruns it after the mask path, before the plain one, and ends a duplicate after it.commitMaskTransformtakes the corners path withDistort Layer Mask,displayedMaskPlacementits corners cases,nudgeLayermoves the corners with the draft. The overlay's corners constructor setsshowsRotationfalse,hitskips the grip then, and the drawing leaves the stem and disc out;geometryreturns the corners' geometry for a group or the edited layer. The canvas'sDisplayStatecarries the edit's corners, so a moved corner repaints;drawOwndrawsdistortPreviewfirst and takesmaskDistortPreviewfor the mask. A Ctrl-drag on a handle begins the distortion and drags indistortmode with the whitedistortCursor; 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. TransformInspectoris the Move tool'sToolHeaderBar: Auto Select and Show Controls asQCheckBoxes, the numbers in aQScrollAreawithout bars (Swift's horizontalScrollView, 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 checkableQToolButtonwith the panel's link icon, the sampling aQComboBox, Flip H and V and Cancel and Apply asQPushButtons.TransformValueFieldapplies as typed (Swift'sisFinitetest is left out:previewTransformrefuses every transform an infinite number makes), shows the session's number while unfocused and puts it back onFocusOut, 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'sonSubmitandonExitCommand; 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 onPaletteChange, as the panel's. Swift's.id(activeLayerID)remake issynchronizeclearing any focus of its own when the active layer changes.resizeBoxis Swift'sresize, whose name would hideQWidget::resize; itsnumber >= 1guard and the scale'snumber > 0are left out: a side under a pixel is a boxpreviewTransformrefuses. The Layer menu's Transform Layer entry callsbeginTransform(Swift'stransformCommanddoes 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):
DocumentSelectionholds aQPainterPathwhere Swift holds aCGPath,SelectionClipaQImagecoverage;SelectionClip::applyhas no twin, because QPainter has no soft clip:PixelAdjust::coveragedraws the clip's coverage through the layer's inversepixelToDocumentinto a mask surface (smooth, as CoreGraphics filters a mask clip) andPixelAdjust::blendmixes two images through it byte for byte, rounded, where Swift runsCIBlendWithMask. Qt'sunited,subtractedandintersectedhonour 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 isWindingFill(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 andcontainsreads the path's own rule, andQTransform::mapkeeps it. Expand and Contract stroke the outline withQPainterPathStroker(round cap and join, Swift'scopy(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::rectrounds withstd::round, Swift'srounded(). The session's selection state is private with getters and setters (lassoKind,marqueeKind,selectionModeChoice,selectionAntialiased, the two amounts),lassoDraftandheldSelectionModegetters alone;selectToolcancels a lasso, as Swift's does.pressMarqueeKeyandpressLassoKeyarrive with the canvas keys of 7.2;displayedSelectionwith the ants.PixelInvert::runis a byte loop where Swift calls vImage (alpha − colour keeps premultiplied transparency), reading any other format as premultiplied first;invertPixelstakes a completion callback, runs the job throughQtConcurrent::runand aQFutureWatcherwith the project busy, and writes back only when the layer still holds the pixels it computed from (identities compared, Swift's===); the worker catchesExportErroralone, the UI sidestd::runtime_error(the clip, the expanded 1×1 mask, the thumbnail), each a warning onomaphoto.appandbrushError.invertPixelsmovesbrushRevisionafter its step, as Swift's does.addMaskfrom a selection draws the outline through the inverse ofpixelToDocumentinto the layer's own grid; a blank layer's grid is its rounded size, as Swift's.deleteLayerOrMaskis the session function the panel's trash, the Layer menu's Delete and (from 8.x) the Delete key share. The loaders inMaskTracing.cppwarn onomaphoto.appwhere 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'sCGImagealready exists. The Select menu and the Image menu's Invert sit between View and Layer, as macOS placesCommandMenus; Select All in a text field is the field's, like Undo (QLineEdittakes Ctrl+A itself throughShortcutOverride, 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, asNSTableView'sselectAll(_:)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'sloadMode), the row untouched; with Ctrl held over an enabled thumbnail the list showsCanvasView::loadSelectionCursor, a pointing hand drawn as paths with Swift's dashed badge, sinceQCursor::pixmapis empty for a shape cursor.LayerMaskMenucallsaddMaskand 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 inEditorCanvas+Cursors.cpp. The canvas tracks Shift as it tracks Alt and Ctrl (the application filter sees the key from any window, as Swift'sflagsChangedmonitor), and a key change reshapes a Marquee draft and the cursor's badge at once. A press with a selection tool islassoMouseDown; a move ismoveSelectionTool(a hover moves a polygonal draft's rubber band and refreshes the held keys, as Swift'smouseMoved); a release isreleaseSelectionTool; the draft's keys and the outline's nudges areselectionKey, ahead of every other key; losing focus isendSelectionGestures. A double click closes a polygonal outline frommouseDoubleClickEvent, since Qt delivers the second press there (its first press adds the corner, as Swift's firstmouseDowndoes), 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 keepsmoveSelectionCursorunder Space, as the transform drag keeps its own. The ants are a 120 msQTimersteppingTransformOverlay::antsPhase, repaintingselectionRectalone (the outline's own rect, not the handles beside it);drawnRectspans 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);synchronizeDisplayrepaints 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 msQTimerwith Swift's speed curve; each step translates the viewport, drags the draft or the outline again under the still pointer, notifies and synchronizes.moveSelectionCursor,selectionCursorandwandCursorare 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), sinceQCursor::pixmapis empty for a shape cursor; the icons areRendering/SelectionIcons, which the rail'sToolIconsand the cursors share, so nothing inRendering/includesUI/.SelectionIconis that header's enum, not nested inCanvasView. Ctrl over the selection shows the move pointer until 7.4 brings the scissors; the wand's click and cursor are 7.3's.pressMarqueeKeyandpressLassoKeychoose 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 (itscycleToolModeon 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.LassoControlsis the selection tools'ToolHeaderBar: Swift's segmented pickers are checkableQToolButtons in aQButtonGroup, the amounts areSelectionAmountFields (aQLineEdittaking 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 asTransformValueFielddoes), the wand's controls arrive with 7.3.ContentView::hinttakes aToolIconKindfor 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.ccompiles in place beside the other kernels, andDocument/MagicWand.cppwraps it as Swift'sMagicWanddoes:selectdraws the image into a premultipliedRGBA8888context (the kernel's byte order, rows top down), floors the point, clamps the tolerance and hands the mask towand_mask;outlinefrees the kernel's arrays throughunique_ptrand builds aWindingFillpath from its loops.MagicWandErrorcarries Swift's two messages (what()),tooDetailedfor the kernel's −2 andmemoryfor −1. The session'smagicWandtakes a completion callback and runs the job throughQtConcurrent::runand aQFutureWatcherwith the project busy, like the invert; the worker catchesMagicWandErrorandExportError, 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 matchsetSelectionwithout the canvas clip (Swift's shortcut), the other modesapplySelection.wandSampledraws the composite throughdrawLiveCompositeor the active layer's own pixels throughLayerRenderer::drawat 1:1, a folder or blank layer as clear; a context that cannot be allocated logs and refuses, as Swift'stry?returns nil.wandSettingsis a struct with a setter that notifies;titleandradiusstand for Swift's computed properties ofWandSampleSize. The canvas hands a wand press tomagicWandwith the press's mode, and a click inside the selection tomagicWandin New mode where another tool would deselect.LassoControlsshows the wand's controls with the wand alone: the tolerance is aSelectionAmountFieldbounded 0–255, the sample size aQComboBoxof Swift's titles, the source two checkable buttons, Contiguous aQCheckBox;changeWandrewrites one field of the settings. The mutant that pinsm_thisLayerchecked is equivalent, since the exclusive group's laterm_allLayersline decides, as with the marquee's kinds. - The clipboard (7.4):
SelectionClipboard.cpptwins Swift's extension as far as the raster edits exist.PixelClipboardandCopiedPixels(Swift's tuple) live inDocument/SelectionClipboard.h.renderSelectedPixelsandrenderMergedPixelsdraw into the copy region and multiply the selection's coverage in withDestinationInthroughBrushRaster::alphaView(clipTo), where Swift clips the context (SelectionClip::applyhas no twin); a mask copies as opaque gray withLayerMask::backgroundfor its ground when placed apart andLayerRenderer::drawCoveragefor 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.storegives the system clipboard the image and a PNG throughQMimeData, and counts the clipboard'sdataChangedsignals from the first copy as Swift reads the pasteboard's change count: a paste puts our pixels back in place while the count still matches, else takes another app's image throughQMimeData::imageData, converted to sRGB first when it carries another colour space (CoreGraphics converts as it draws;QPaintercopies the numbers), then to the working format, tagged sRGB and centred. A cleared clipboard has noQMimeDataat all, socanPastechecks the pointer. A working copy that cannot be allocated is a warning, as Swift'stry?beeps there, where a failed Layer via Copy setsbrushErroras Swift'scatchdoes. The clipboard is touched only by a copy or a paste, so session tests without one run underQTEST_GUILESS_MAINstill;SelectionClipboardTestsruns underQTEST_MAIN.addPixelLayer's shape and text arrived with 8.8 and 8.9a. Swift'scutSelection,PixelMoveand the wholeFloatingSelectionextension needclearSelectedPixelsand the brush raster, so the cut arrived with 8.2 and the rest with 8.2b, wherecanEditLayersandProjectWorkspace::canSwitchgained theirpixelMoveterms. 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 followscanCopyMerged; the Layer menu's Ctrl+J islayerViaCopy, titled by the selection and enabled as Swift's:canCopyPixels, or no selection on an editable layer that is no folder. Swift's beeps are warnings onomaphoto.app. - Content-Aware Fill (7.5):
ContentFill.ccompiles in place beside the other kernels;Document/ContentFill.cppincludes its header in angle brackets, because the twin's own header shares the name.ContentFill::rundraws the job's image into a premultiplied context and takes the pixels to make fromPixelAdjust::coverage(the selection through the inverse mapping, as Swift clips the mask context and fills white), the kernel's 0 isContentFillError(noSource)with Swift's message and its −1ExportError(render).Document/Filters.hholds what Swift'sFilters.swiftholds as far as it exists:FilterKind(rawValueandisAutomaticare 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, thenPixelAdjust::blendthrough the selection) andFilterEdit, a plain class in astd::optionalof the session, since only the session writes it; itsidstands for Swift's object identity, so a preview lands on the edit it was made for.FilterEditgrows throughgrow(the blur's growth and the preview copy came with 9.4a). The session's extension isFilters+Session.cpp: Swift's file holds 473 lines, its twin's two halves about 500.canContentAwareFillwithout itsisMaskSelected,filterEditandisEmptyterms (canAdjustColorsholds all three; it sits inHueSaturation.cpp, the twin of its Swift file, alone until 9.3, with the terms that exist),beginFilter(cancelCropwaits for the crop),updateFilter(its settings came with 9.4a),cancelFilter,commitFilterwith a completion callback,finishFilterCommit(Swift's guards, the mask carried throughLayerMask::clipImage). Swift'spreviewTaskis oneQFutureWatcher:renderFilterPreviewmarks 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 inm_filterCommitWaitingwith its edit's id and called again fromfinishFilterPreview(every waiter is, and one whose edit has gone just returns); the wait is onpreparing, which only the delivery clears; Preview off is 9.4a's; the workers catchContentFillErrorandExportErroralone.brushRevisionmoves where Swift's does (a delivered preview, Preview switched either way, a cancel, a commit's end), so the canvas'sDisplayStatesees each new preview.displayedTransformanddisplayedMaskPlacementtake the grown transform while the edit previews, and the canvas drawspreviewImagein the layer's place.canEditLayersandProjectWorkspace::canSwitchreadfilterEdit.FilterSheet(afilterPanel's content since 9.4b): the kind's words, Preview, the error inBrightText, "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'scancelActionandonClose. The Edit menu's Content-Aware Fill… is Shift+Backspace (Swift's.deleteis that key) and followscanContentAwareFill; the canvas takes Shift with Backspace or Delete ahead of the selection keys, as Swift'skeyDowndoes. - The brush on the CPU (8.1):
Rendering/BrushCoveragetwinsMetalBrushCoverage: the shader's maths in float, pixel for pixel (the centre sample, the hard tip's antialiased silhouette overmax, 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. ATilekeeps 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 isExportError(render). The class with itsshareddevice is a namespace.Document/BrushStrokekeeps 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, andspacingFractionstays because the integral reads it.BrushStroke.swiftruns to 899 lines, so the twin splits:BrushStroke.cpp(BrushRaster, withfill, 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 andpublish) andBrushStroke+Commit.cpp(transform(for:),paintSnapshot,commitInput,BrushCommitas a namespace, since Swift's actor only serialises). The grid is worked out first (Grid) so the const members can follow Swift'slets; Swift's guards throwProjectError(tooLarge)as one, and the extent is judged as a double before it becomes anint, because pastintthe conversion is undefined where Swift'sIntjust holds the number. The stroke's source is the asset itself (m_source, anImportedImage), so a painted layer draws from its tiles and never flattens;commitInputhands the asset on, andBrushCommit::renderdraws it the same way. A selection clips the context in Swift;filltakes the selection'sPixelAdjust::coverageon 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 isExportError(render), as Swift'scroppingreturning nil; so is a tile's context that cannot be copied on publish (QImage::detachleaves a null image where CoreGraphics'makeImagereturns nil). Tile keys (row × columns + column) areqint64, Swift'sInt: a small layer scaled to a point across a big canvas passesint. A tile is rebuilt whole on every publish (dirtyTilesonly ever narrowed the dab fallback's rebuilds);baseis the tile's first pixels, kept apart by QImage's copy on write.BrushSettingsholds the tip and colour;erasing,healingandhealingModearrive with 8.2 and 8.4,selectionClipis here sincepublishreads it,clone,isBlur,replacesWithClone,editName,fillGradient,fill,clearPixels,paintCanvas,liftSelection,moveLiftedandhealwith their items.BrushIntersectionTestsruns the one path where Swift loops overuseGPU;BrushTestsholds the stroke-level halves of Swift's session tests, driven on aBrushStrokemade from a layer, withLayerRenderer::drawBrushPreviewas the live view andBrushCommit::renderas 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.cpptwins Swift's extension as far as the tools exist:canPaintwith the terms that exist (the adjustment term since 9.5a),makeRasterEdit(the budgets, the selection clip;pixelLimitisqint64, Swift'sInt, and may run below zero when other layers use more than the budget),beginBrushfor 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'sasyncwrapper is a plain call here),commitPaintSnapshot(the tiles land at once with their undo entry; a mask that covered the old grid is regrown throughRasterSnapshot::replacing),commitRasterEdit(a completion callback, the render throughQtConcurrent::runand aQFutureWatcherwith the project busy, the worker catchingExportErroralone, the UI side keeping Swift's guards and the mask'sisEnabledas it stands at commit),usesOpacityKeys,typeOpacityDigit(Swift's uptime isstd::chrono::steady_clockunless the caller passes a time, as the tests do),changeBrushHardness,changeBrushSize.brushStrokeis astd::unique_ptrread through a pointer, since a stroke's const members forbid copying;brushSettings,brushMode(BrushToolModeinDocument/SmudgeLiquify.h, whose smear and liquify arrive with 8.6) andmaskPaintWhiteare getters and setters that notify;lastBrushPointandpendingOpacityDigitare private structs.brushRevisionexists after all, Swift's counter, because the canvas'sDisplayStatemust change with every step of a stroke, whose patches it does not hold; a step that changes nothing else repaints the stroke'sdirtyDocumentRectalone, in view points two points out (strokeDirtyRect), as Swift'ssetNeedsDisplaydoes, 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'sliveMaskEdit):drawBrushPreviewdraws 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'sdisplayImagedoes, so a fine mask keeps its shade while painted. The session's private structs (Decoded,Wanded,RasterMadeand the rest) live inEditorSession+Jobs.hasSessionJobsand are aliased in the class, so the header stays under 500 lines. The gates (canInvertandcanAdjustColorsamong them),selectLayer,selectLayers,selectToolandselectLayerTargetrefuse while a stroke lasts,canStartProjectOperationtoo, andimportImagesfinishes a stroke first, as Swift's do; the tips' parking by tool family arrived with 8.5.SelectionEdits.cppgainscanEditPixels,fillSelection(the palette's colours are black and white until 8.10'sColorPalette; on a mask the background is the opposite ofmaskPaintWhite, as Swift'spaletteColor),clearSelectedPixels,deleteKeyPressed(its effects term since 9.6a) andapplyPixelEdit;cutSelectionsits inSelectionClipboard.cpp.BrushStrokegainserasingin its settings (BrushRaster::filltakesDestinationOutfor it, Swift's blend mode on the context; a mask never erases, so that mode on a gray context is alogic_error),paintCanvas(the base redrawn, the body under the inverse mapping and a hard canvas clip, thenPixelAdjust::blendthrough the selection where Swift clips),fillandclearPixels;placedMaskPreviewis declared inBrushStroke.hand defined inLayerMask.cpp, as Swift's extension, and a preview that cannot be made logs onomaphoto.renderingwhere Swift'stry?is silent. On the canvas a pixel stroke draws its whole grid placed bypaintTransformthroughTiledLayerRenderer::drawStroke, a mask stroke throughdrawMaskStrokeunder the layer's transform, and a mask on its own placement draws the layer throughplacedMaskPreview; the mask's clip takes the stroke's owner as Swift'sbasedoes. Delete and Backspace without Ctrl, Alt or Meta aredeleteKeyPressedon 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'sdeleteWordBackwardanddeleteToBeginningOfLine(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, ascanEditPixelsdoes), and Clear Selection Pixels.tests/BrushFixtures.hshares the fixtures;BrushSessionTestsandPixelEditTestshold the session halves of Swift'sBrushTests,RasterSnapshotSessionTeststhe five session cases,BrushCanvasTeststhe twocanvasShiftcases and the canvas's Delete key. - Moving and floating selected pixels (8.2b):
PixelMoveis a class inDocument/SelectionEdits.h, Swift'sfinal class. Its raster is astd::shared_ptr<BrushStroke>, because the canvas draws it while the commit runs, socommitRasterEdittakes astd::shared_ptr<const BrushStroke>(the brush hands its stroke over); itsoffsetstarts at zero, since a defaultQSizeFis −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),cancelPixelMoveandnudgePixels(Swift's beep is a warning onomaphoto.app) live inSelectionEdits.cpp;displayedSelectionfollows a pixel move and a floating edit.BrushStroke::liftSelectiondraws the source into the region, from its tiles when painted, and multiplies the selection's coverage in (PixelAdjust::coverageunderDestinationIn) where Swift clips;moveLiftedrebuilds each tile from its base, cuts the hole through the same coverage underDestinationOut(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.cpptwins Swift's file:FloatingTransformrides inTransformEdit.floating(last) as astd::shared_ptr<const FloatingTransform>, because the edit is copied freely and holds the whole document from before;canTransformSelection,transformCommand,beginSelectionTransform(a completion callback: the outerTransform Selectionedit opens,clearSelectedPixelsruns, and its callback inserts theFloating Selectionlayer),floatingSelectionTransform,mergeFloatingTransform,cancelFloatingTransform, andFloatingMerge::merge, a namespace with aMergedstruct 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::mapPathcame forward from 6.2's list: it walks the path's elements (Qt stores a quad as a cubic) and keeps the fill rule.beginPixelMoveandcanTransformSelectionleave out Swift'spixelMove == nil,transformEdit == niland!selection.isEmpty, whichcanPaintreads already (throughcanEditPixels).displayedMaskPlacement'sfloating == nilterm is dead and left out: the floating layer is made without a mask, and the open edit blocks adding one;beginDistortedits the edit in place, sofloatingcarries over. The session's private structs sit inSessionJobs, now a private base struct ofEditorSession(the aliases went;MergePlanandTransformDuplicatejoined 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, undermovePixelsCursor(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;updateCursorpicks the outline and pixel drags' cursors ahead of Space, solassoCursor, 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;drawOwndraws 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 istransformCommand, titledTransform SelectionwhilecanTransformSelectionholds and enabled by either gate.canEditLayers,canInvert,canAdjustColorsandProjectWorkspace::canSwitchreadpixelMove. - The brush on the canvas (8.3):
EditorCanvas+Brush.cppholds Swift's gestures. A press begins a stroke, Shift drawing on fromshiftLineStartthrough 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, thenfinishBrushImmediatelycommits. 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'smouseDragged(a hover after a busy release paints nothing), and wheels and pinches neither pan nor zoom while it lasts, as Swift'sscrollWheelandmagnifyrefuse. 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.BrushCursorOverlayis no widget, likeTransformOverlay: 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);updatereturns the old and new rings three points out, as Swift'ssetNeedsDisplay. Swift'shardness > 0is dead (a zero ellipse strokes nothing) and left out. The circle follows the pointer, hover included but not a pan (Swift'smouseDraggedpans first), hides under Space, on leaving and on a lost focus, and shows with the brush tools alone. The canvas'seventtakes Tab before the focus chain (Shift-Tab stays the chain's) forcycleToolMode, 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 reachestypeOpacityDigitonly whereusesOpacityKeysholds, else it goes on to the parent, as Swift'ssuper.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'skeyPressEventre-reads the modifiers as Swift'skeyDowndoes. The layer list takes Tab and Shift-Tab (Swift's table cycles on both), B, E and digits alike.BrushControlsis every brush tool'sToolHeaderBar: 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 underQSignalBlockerso nothing rounded is written back, the fields stepping with Up and Down from the session's exact value as Swift'sarrowStepsdoes (a slider's 33.3% steps to 34.3%;SelectionAmountFieldtakes Swift'svalueandchangeclosures, and the Lasso and Wand fields round inchangeas 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.cppholdspercent,iconKindandhint, the eraser's words and icon among them;iconKindchecks its tool although every consumer ignores a kind that is not its own (an equivalent mutant).updateCursorand the modifier helpers moved toEditorCanvas+Cursors.cpp.ContentView's destructor stops listening first:~QWidgetcloses a shown window after the members are gone, and the canvas's focus-out then notifies (cancelBrushbumpsbrushRevision, 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
tipDensityevaluates 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 forexpandlog.BrushCoverageTestsholds 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 inQUnhandledException).publishcopies 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 ablockingMapinside 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.cppandDownsampleCache.cppbuild 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 andtipDensity's two early returns (each only costs time).BrushPerformanceTestsruns underBRUSH_BENCHMARK=1. - Canvas speed (11.1):
CanvasPerformanceTests(underCANVAS_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::blendran 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, sincescanLinedetaches), 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 withdocs/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 everyQtConcurrent::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, thenreserveThread, Qt's documented remedy);PoolMapTestsruns 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 whichmaingives 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'sStartupWMClassisOmaPhoto.packaging/io.github.ZacharyZhang_NY.OmaPhoto.desktoplaunchesomaphoto %F(files named at launch reachProjectWorkspace::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.xmldeclares 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 --installlays 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 undershare/licenses/omaphoto;OMAPHOTO_MODEL, a path, installs the model asshare/omaphoto/u2net.onnxwhatever its own name, the file itself when given a link (file(REAL_PATH)), whereSubjectRemovallooks, and packages pass it (11.3). A direct install (noDESTDIR) runsupdate-mime-databaseon its prefix, required, so the project type registers at once; a staged install leaves that to the package's hooks.DesktopIntegrationTestsreads 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-inupdate-mime-databasefirst on the path and anyDESTDIRof the caller cleared; the call inmainis 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
LICENSEand U²-Net's, undershare/licenses/omaphoto), andOMAPHOTO_MODELadds the model.OMAPHOTO_BUNDLE_ORT(off by default) installs ONNX Runtime beside the app inlib/omaphoto(under the library folder) with the app's run path set there, and Microsoft'sLICENSEandThirdPartyNotices.txtwith 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/localtoo, so the tests run against the runtime that ships; the flake builds from nixpkgs' own, since Nix rebuilds its closure with it.DesktopIntegrationTestsexpects 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 fromdpkg-shlibdeps, which finds the bundled runtime through the run path, and it names what no library does (libheif-plugin-libde265for HEVC,qt6-image-formats-pluginsfor TIFF,shared-mime-info,hicolor-icon-theme); the RPM asks forqt6-qtimageformats,shared-mime-infoandhicolor-icon-theme, its libraries found by rpmbuild but the bundled runtime left out of what it provides and requires (__provides_exclude_from,__requires_excludeinCPACK_RPM_SPEC_MORE_DEFINE), and suggests RPM Fusion'slibheif-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.devgainsdpkg-dev,fileandfakerootfor the DEB.packaging/arch/PKGBUILDbuilds this checkout for Arch and Omarchy (makepkgthere), fetching the model and Microsoft's runtime by their checksums; itsprepare()lays the runtime out as that build's CMake config expects (lib64,include/onnxruntimebesidelib/cmake) and the package bundles it.flake.nixsits 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|resolutebuildsscripts/distro/<name>/Dockerfile(forresolute,Dockerfile.devon 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 andHOMEandTMPDIRon 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 committedflake.lock) as root, since Nix writes its store, then runs the tests in itsdevShells.defaultas uid 1000 (chroot --userspec, withHOMEandTMPDIRput back, sincenix developpointsTMPDIRat 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::flippedfrom Qt 6.9 where 6.4 hasmirrored;Q_OBJECTon every class a test finds withfindChild(Qt 6.11 refuses one without);DistortWarpprojects points itself (projectinDistort.cpp), sinceQTransform::mapclamps 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, andDistortTestspins 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-deleteinDownsampleTests' 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), andQT_FORCE_STDERR_LOGGINGfor 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 (placesStarvationinAddressSpaceLimit.h); the limit itself holds there.ProjectExportFailureTestsopens a panel and an alert once ininitTestCase, 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 wholeconf.dwithout 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.theJPEGRenderTakesTheSnapshotWithoutACopyholds the JPEG render's snapshot to a move under a 2 MB limit, asthePanelOpensWithoutCopyingTheSnapshotholds the PNG panel's. The flake leaves outdev.sh'sbuild/, which nixpkgs' CMake hook would take for its own build folder. - Spot Healing (8.4):
HealPixels.ccompiles in place beside the other kernels.SpotHealingMode,allSpotHealingModesandrawValue(Swift's names) sit inBrushStroke.h, andBrushSettingsgainshealingandhealingMode. While the stroke lasts its tiles show Swift's wash, gray 0.12 at 45% through the coverage, never erasing.healtakes the painted bounds from the kernel'sheal_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 isQRandomGenerator::global(), Swift'sUInt32.random. Each touched tile is then its base with the healed region copied over, blended with the base throughPixelAdjust::blendunder a selection, where Swift clips the context. The kernel's failure isProjectError(tooLarge); pixels or packed coverage that cannot be allocated areExportError(render), as Swift's contexts. The session'sbeginBrushtakes Spot Healing on pixels alone, never on a mask, as Swift's guard;finishBrushImmediatelyheals before the commit, oneSpot Healingstep;spotHealingModeis a getter and a setter that notifies; Tab steps the type.isBrushToolis Swift's four tools: the circle, the right-drag tip, the brackets, the digits andBrushControlsserve each, and a press with Clone Stamp or the Smear begins nothing until 8.5 and 8.6 bring them.ContentViewkeeps one bar across the four, as Swift's singleisBrushToolbranch 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.hholdsCloneSettings,CloneStamp.cppSwift's extension:setCloneSource(a point that is no number is refused; a new source restarts the alignment),cloneStrokeOffset,cloneSamplePointandcloneSample, whose context that cannot be allocated logs onomaphoto.appwhere Swift'stry?is silent. The session keepscloneSource,cloneSettings(a setter that notifies),cloneOffsetandparkedBrushTipsbytipFamily(Clone Stamp 1, the Smear 2, the rest 0; the two parked tips start at Swift's 40, 0, 1).selectToolparks size, hardness and opacity when the family changes and reads the tip it restores withat: the session starts in family 0 and leaving a family parks it, so Swift'sif letalways finds one.beginTransformand Transform Selection set the Move tool directly, as Swift's do, and park nothing.beginBrushtakes 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 isClone Stamp.BrushStroke::Cloneis Swift's tuple.publishmakes 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 byBrushRaster::shiftedByFraction, since QPainter snaps an unscaled image.fillClonethen composites it source-over through coverage × opacity × selection, each byte rounded once, where Swift clips the context to the coverage. Swift's!isMask || isBlurholds since 8.6: a mask takes a clone only from Blur. The bar shows Aligned (aQCheckBoxwritten onclicked, so the session's own changes write nothing back), This Layer and All Layers, and "Alt-click to set the source" inPlaceholderTextwhile 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.updateBrushCursorgives 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.mediumthere is overridden byLayerRenderer's own quality and left out) withcloneTip(one click painted by the brush engine, kept by size and hardness).BrushCursorOverlaygainsmarker,preview,previewOpacityandtip, images compared bycacheKeyas Swift's!==; under an antialiased ellipse clip at the brush's opacity it draws the preview and cuts it to the tip insideAdjustmentSurface::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:displayedTransformincloneSampleandclonePreview(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/.cppholdBlurToolModeandWarpStroke, Swift's float arithmetic to the letter (the positive guards ofweightincluded) 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.beginWarpandfinishWarptwin 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,replacesWithCloneso clear pixels clear, one step named by the mode.BlurTool.cppholdsblurSample: 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 isPixelAdjust::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.BrushStrokegainsisBlur(a mask takes a clone only from Blur),replacesWithClone(a copy by coverage:fillClonekeeps 1 − coverage of the old pixel) andeditName(the step's name when set).clonePixelsresamples every clone, a gray one too, into premultiplied colour, so a sample's edge keeps the share of the pixel the image covers, andfillClonecomposites 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 keepsblurModeandwarpStroke;beginBrushhands Liquify and Smudge tobeginWarp;continueBrush,cancelBrushandfinishBrushImmediatelytake the warp first; exactly Swift's six places read it:canEditLayers,canUseHistory,canStartProjectOperation,selectLayer,selectTool,cycleToolMode(selectLayersandselectLayerTargetread 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" (hinttakes 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:displayedTransforminbeginWarpandblurSample(painting refuses transform and filter edits), the radius rounded to nearest (pixels past its floor lie outside the brush),u >= hforu > 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.cpptwins Swift's extension.GradientEditis a struct in astd::optional, where Swift's is a class; its raster is astd::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'sgradientEdit === edit(a new line on the same target keeps the raster, as Swift keeps the edit).commitGradienttakes a completion callback, posted on every path;resolveGradientposts the commit, Swift'sTask, so a switch of tool, layer or target applies it once the caller returns;invertPixelsandbeginFiltercommit it first and call themselves again from its callback, as Swift awaits it.fillGradientfills the canvas throughpaintCanvaswith aQLinearGradient, or aQRadialGradientcentred on the start with the line as its radius; Qt's pad spread is CoreGraphics'drawsBeforeStartLocationanddrawsAfterEndLocation, and two stops interpolate alike in both.gradientColorsmakes a mask's colours gray from the palette's red, where Swift makesDeviceGraycolours.undofirst discards a pending gradient andcanUndocounts one, as Swift's. The palette's model came forward from 8.10 intoColorPalette.cpp(PaletteColor,foregroundColorwithsetForegroundColorthroughsetBrushSettings,backgroundColor,canEditPalette,paletteColor,setPaletteColor,swapPaletteColors,resetPaletteColors), andfillSelectionreads it; the Type bar's share ofsetPaletteColorcame 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.TransformOverlaydraws 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 throughliveFolderMaskClip, as a brush stroke does.GradientControlsis the tool'sToolHeaderBar: the shapes as checkableQToolButtons in aQButtonGroup,GradientSwatch(white, gray checks of four points, the colours, clipped to a rounded rect, the rim unclipped as Swift'sstrokeBorderoverlay), the styleQComboBox, Reverse, the slider in thousandths from 1% and the percent field (aSelectionAmountField, whose own clamp stands for the binding's), the Mask note inPlaceholderText, and Cancel and Apply while pending. Equivalent mutants: the gray branch ofgradientColorsand offillSelection(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/.cpptwin Swift's file,ShapeTool+Session.hits session half.ShapeKind's raw values,shapeKind(text)for the manifest andpathstand for the enum and itspath(in:cornerRadius:);LayerShapeStyle,LayerShapeandShapeDraftkeep Swift's fields,ShapePreviewis Swift's tuple. ALayerShapeholds its image'sImageIdentity, Swift's===, sinceImportedImage::image()is never cached in a model type;ImageLayer.shapejoins equality, andliveShape()compares identities, so any other change to the pixels leaves a shape that is no longer live.addPixelLayertakes the style and ties it to the asset it makes, where Swift passes aLayerShapemade from the same image.shapeKind,shapeCornerRadiusandshapeLineWidthare getters and setters that notify,shapeDrafta getter.finishShapetruncates each side before multiplying, as Swift'sInt(), in doubles; a Line click leaves a dot as wide as the line, since the box grows by the thickness, as in Swift.strokedLinegives 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.redrawShapepins any mask where it shows (Swift'splacement == niltest changes nothing: a placed mask'smaskTransformis its placement) and logs onomaphoto.appwhere Swift'stry?is silent;shapeTransformPreviewisconstwith a mutable cache, reads the pixel size from the asset a live shape shares, and logs onomaphoto.rendering. Left out as dead or redundant:path'smax(0, …)(itsradius > 0test sends a negative radius to the plain rectangle),shapeLineEnds's kind test and the draft drawing'sguard let ends(only a line's drag sets an end, and its box with it),finishShape's zero-width fallback inunit(the box is a pixel wide by then),redrawShape'smax(1, …)(a valid transform is a pixel wide), and the canvas drag'slastDragPoint == nil(a pan never begins a draft). Hooks:selectToolcancels a draft, Tab steps the kind,commitTransformredraws each moved shape, the record carries a live shape's style (hierarchyRecord),installProjectloads shapes andapplyDocumentSizedoes 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'sshapekey is Swift'sCodable: the kind's raw value, the numbers,lineWidth,startandendonly 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'sdrawOwn, composed aside throughLayerRenderer::compositeso 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'ssynchronizeDisplaycomparesShapeShown(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;DisplayStatecounts 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 drawsshapeTransformPreviewin place of its pixels.ShapeControlsis the tool'sToolHeaderBar: the kinds as checkableQToolButtons in aQButtonGroup(a choice drops the draft first), Width for lines (a slider of 1–100 showing at most 100, aSelectionAmountFieldof 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'saddRoundedRectadds a plain rectangle at radius 0, soradius > 0and>= 0agree; an undragged draft's box is its anchor, sofinishShape'svalue_ormay 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/.cpptwin Swift's file,TypeTool+Session.hits session half.TextAlignment's raw values andtextAlignment(text)serve the manifest;LayerTextStyle,LayerTextandTextDraftkeep Swift's fields. ALayerTextholds its image'sImageIdentity, as aLayerShapedoes;ImageLayer.textjoins equality andliveText()compares identities. TextKit has no Linux twin, soRendering/TextLayoutis Linux-only:TextLineslays a style out withQTextLayoutastextBoxSizeandtextImagelay it out with TextKit. Lines have a fixedlineHeightwith 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, asNSParagraphStyle'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 anNSTextView); 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, whileQTextLine::setNumColumnsleaves the tab hanging on the line above. Faces go by PostScript name, as macOS names them: fontconfig lists each installed face'sFC_POSTSCRIPT_NAMEwith 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::engineForScriptrounds 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.devinstallslibfontconfig-dev; CMake linksFontconfig::Fontconfig. In the sessiontextDrafthas a public setter, as Swift writes it freely, andtextDefaultsa getter.applyTexttakes its draft by value, as Swift's struct is passed:finishTexthands 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.layerNamekeeps 40 graphemes (QTextBoundaryFinder), Swift'sprefix(40)of Characters;simplified()makes whitespace runs one space.recolorTextlogs a failed redraw where Swift'stry?is silent. Left out as dead: theisFiniteterms ofisValidandboxIsValid(the ranges refuse NaN and infinities); the draft and document guards ofbeginText,editActiveTextandrecolorText(canEditLayersreads both);textBoxSize'smax(16, …)on both sides (the padding alone makes 24) and its caret line (an empty paragraph lays a line);textImage'swidth >= 1terms;recolorText's validity guard (textImagerefuses 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'sliveTexttest (recolorTextasks it); the store rule's folder term (text needs pixels, andLayerHierarchy::validaterefuses a folder that has them). Hooks: the gates andcanStartProjectOperationreadtextDraft;selectLayer,selectLayersandselectToolfinish 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;addPixelLayertakes the style, as it takes a shape's;installProjectloads text andapplyDocumentSizedoes 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'stextis Swift'sCodable: every key required butboxSize(synthesized decoding ignores the defaults), the alignment by raw value, the box as two numbers; the store refuses a project whose text failsisValidor has no pixels (Swift'svalidate; 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).TypeControlsis the tool'sToolHeaderBar: the face as aTypeFontPicker(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 asTextStyleFields,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 anNSTextFielddoes: Swift's Type bar alone has noreleasesFocusOnCommit; 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 checkableQToolButtons with Swift'stext.alignglyphs drawn as paths, redrawn onPaletteChangeas 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 andopenTextColorPickercame with 8.10a. A live text row in the panel shows the rail's Type glyph 1.2 times up, centred (Swift'stextformat, 1.21 times up), "Editable text layer", "Select text: " and "Text" (Swift 1.1.5's words);ThumbnailKeycarrieseditableText, so text that loads as pixels redraws. A double click on a control makes the row active, then opens its text (Swift'srenameClickedLayer); 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);TextLineswithout 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/.cpptwin Swift's file;InlineTextEditor+Editing.cppholds its text view's keys, moves and undo,InlineTextEditor+Input.cppits input methods and clipboard. Swift's editor hosts anNSTextViewthat AppKit turns and mirrors with the layer; a Qt child widget cannot turn, so the canvas paints the editor itself, as it paintsTransformOverlay, and hands it its keys and input methods, and whatNSTextViewgives for free (selection, typing, deletions, movement, undo, the clipboard, marked text) is written here onTextLines, the layouttextImagedraws.synchronizefollows Swift's: the logical size istextBoxSize, 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 everychanged(), so a draft that ends takes its editor with it; Swift'sfreshbranch is reached here only when the session'ssetTextDraftsets 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; 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 isHighlight, the inactive one without focus, drawn per line from Qt's own visual runs (aFormatRangebackground, 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.TextLinesgainsTextRange,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),positionand 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 owncreateLinemakes 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 (lineOfis empty pastm_laidEnd, where Qt'slineForTextPositionwould 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, asNSTextView'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 whatQInputControl::isAcceptableInputaccepts, term for term (a format character before the Ctrl test, AltGr's Ctrl with Alt typing). Characters are graphemes and wordsQTextBoundaryFinder'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'sremoveAllActions). Marked text lives in the draft's content, asNSTextViewkeeps 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 followsQWidgetLineControl::processInputMethodEventon 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). ASelectionattribute sets the selection after the commit; while a preedit shows, that selection stays beside it (the preedit'sanchor, 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;TextFormatattributes style the preedit, counted from its start (a format that is none is passed over, as Qt's fields do), else it is underlined; aCursorattribute places the caret, hides it at length zero and colours it with aQColorvalue. 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 insynchronizeDisplay(Swift'ssynchronizeInlineText, whose twin lives inInlineTextEditor.cpp), skips the edited layer indrawOwn, the editor's repaints covering its box while it is edited; a draft that begins, ends or is replaced repaints the whole canvas, as Swift'sneedsDisplay, 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 toQWidget::keyPressEvent, which ignores it for the ancestors, none of which takes keys: its mutant is equivalent; so is the blend keys' focus term, sincecanEditLayersrefuses 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) winShortcutOverrideahead of the menus, Shift+Tab stays put, focus shows and hides the caret (its blink iscaretBlink, a named child timer of the canvas, running only with focus, so tests read it), input methods are enabled throughsetAttributealone (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, asQWidget::updateMicroFocus, and a focus request leaves the keys where they are, as Swift'sconsumeFocusRequest. 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.cppholds the pointer half of Swift's file: the canvas's text gestures (theCanvasViewextension there:beginTextGesture,dragTextGesture,finishTextGesture,drawTextBoxDraft) and what the editor view and itsNSTextViewdo with the mouse. A Type-tool press goes to the open editor first (press, Swift'shitTest): a handle's band (Swift'shandle(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; elsewherebeginTextGesturefinishes 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 forwardsmouseDown), or starts a box. A release under four pixels both ways is point text, elsebeginText(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'sneedsDisplayat the end ofbeginTextGestureis 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 indragTextGestureanddrawTextBoxDraft(the twin's intextBoxDraftRect, which both read). Clicks followNSTextView: one places the caret at the nearest position, two select the word under the pointer (QTextBoundaryFinderwords; the character under it byCursorOnCharacter,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, whereNSTextViewputs 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 anint: a far drag over a squashed layer passesint'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, asQWidgetTextControltimes 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'sNSTextViewtakes 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, asNSTextViewunmarks. A handle's press makes point text a box of the size it shows (Swift'smouseDown, whosefixed.originis 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'shandleCursoron Qt's four resize shapes; the rotation is reduced by 360 before the eighths are counted, since Swift'sIntholds turn counts anintcannot), 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 sendsmouseUpto the view that tookmouseDown; 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.arrowPathandduplicateCursormoved toEditorCanvas+Cursors.cpp, keeping the navigation file under 500 lines. - The palette and its picker (8.10a):
Document/ColorPalette.h/.cpptwin Swift's file.PaletteColorgainsquantized,hexandfromHex(Swift'sinit?(hex:)): spaces and tabs trimmed as Swift's.whitespacestrims them (Unicode space separators and the tab, never a line break), one leading#, three digits doubled, six required, then Swift'sUInt32(_:radix:)read by hand: an optional sign, a minus holding zero alone, no0x(Qt'stoUInttakes0xand white space).PickerHSBkeeps Swift's maths to the letter (fmodfortruncatingRemainder,Int(h)truncating) and itssetRGB: grays keep the hue, black the saturation.ColorPickerTargetis a struct with aKind, asTransformDrag::Modetwins an enum with payloads: the palette and text, the Gradient Map's ends since 9.4a, an effect's colour with 9.6.ColorPickerStateis a plain struct in the session with anidfor Swift's object identity (a new picker opens its panel anew); Swift's unusedbackgroundandPaletteColor(NSColor)are left out. The sheet writes the working colour throughsetColorPickerHSB, which notifies, where Swift writescolorPicker.hsbin place;closeColorPickerclears the picker before applying it, so each signal sees it closed.sampleCompositeColordraws the live composite into one premultiplied pixel at the floored point and undoes the premultiplication as Swift does; a composite that throws logs onomaphoto.appand samples nothing.UI/FloatingPanelis Swift'sFloatingPanelController, brought forward from 9.1: aQDialogwithQt::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'scloseandwindowWillCloseremember again); first shown over the canvas's middle (the owner window'sCanvasView), else centred on the window, as aQDialogcentres 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'smakeKeyAndOrderFront; its close button and Escape report throughonClose, aswindowWillClosedoes, andclosehides without reporting (Qt'shidesends no close, so Swift'sdismissinghas no twin);refocusactivates 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::closeEventwould 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;hidesOnDeactivatehas no Linux twin.UI/ColorPickerSheettwins 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'sDragGesture. 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 arePickerFields, 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'sonChange(of: hexFocused), which snaps the working colour to its 8 bits. Return in an entry commits it and goes on to OK, since aQLineEditpasses 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.ColorPickerPanelControllershows 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/ColorPaletteControlstwins 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).SwatchButtondraws 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 showstypeColorand opensopenTextColorPicker. X and D swap and reset the palette on the canvas and in the list, Shift allowed, not with Ctrl, Alt or Meta;ProjectWorkspace::canSwitchrefuses 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'sisVisible(offscreen activates no hidden window),show'swasVisibleguard (a shown panel stands where it was remembered), and insampleCompositeColorSwift'smin(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.cppholds Swift's sampling.palettePickingis the Eyedropper, or Alt with the Brush, Spot Healing or the Gradient while no stroke runs and no gradient end is dragged;pickingadds 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).sampleColorsamples into the open picker, else sets the foreground throughsetForegroundColorwhile the palette can be edited (a chosen mask too, as Swift setsforegroundColor), 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'shitTest), a hover with no button held (the eyedropper alone: the brush circle neither moves nor shows, as Swift'smouseMovedreturns first; a right-button drag still sizes the tip, as Swift'srightMouseDraggedignores picking) and the cursor (after the drag cursors, before Space).syncPickingends a sampling when picking begins or ends:synchronizeDisplayruns 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).pickingReleasereturns nothing, where Swift returns frommouseUp: a sampling press begins no other gesture, so the rest of the release finds nothing to end.SampleRingOverlayis no widget, likeBrushCursorOverlay: 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 inContentView: the title and a "Sample Ring"QCheckBox16 points on, following the session under aQSignalBlocker.eyedropperCursordraws 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 isRendering/EyedropperIcon, which the rail and the cursor share, as they shareSelectionIcons. A mounted canvas leaves the keys to an open picker (Swift'sviewDidMoveToWindow). The application event filter moved, unchanged, beside the modifier helpers inEditorCanvas+Cursors.cpp, keeping the navigation file under 500 lines. Equivalent mutant: the ring'ssetBrush(Qt::NoBrush)(the painter reaches it with no brush). - Levels in the session (9.1a):
Document/Levels.h/.cpptwin Swift's file,Levels+Session.hits session half;LevelsAutomatic.h/.cppandLevelsAutomatic+Session.hthe other file.LevelsPixels.ccompiles in place beside the other kernels.LevelsChannel,LevelsSampleandLevelsAutokeep Swift's raw values (rawValue) and case order (allLevelsChannels,allLevelsSamples,allLevelsAutos);LevelsSampleis declared opaquely inLevels.h, sinceLevelsEditholds one, and defined with the automatic levels, as Swift's.LevelsSettings::samplingis declared inLevels.hand defined inLevelsAutomatic.cpp, as Swift's extension;settings(mode, histogram)stands forLevelsAuto.settings(histogram:);setCurrentis Swift'scurrentsetter, storing the range normalized. A histogram isLevelsHistogram, four arrays of 256 bins where Swift has[[Double]], andLevelsHistogramDisplay::scaletakes one channel's bins.LevelsFilter::runleaves soft edges to the kernel. Swift's cc5aac4 once unpremultiplied each soft edge beforelevels_apply, which unpremultiplies again; Swift agrees with the twin since 1.2.0 (f6a74c9removed that pass), and itsinputClippingGammaOutputInversionAndAlphaholds (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 throwstd::bad_allocpast the worker's catch.LevelsEditis a plain class in astd::optionalof the session, likeFilterEdit, itsidstanding 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'ssetFuturedrops a replaced future's pending signal, andisRunningandisFinishedboth end with the worker (probed on 6.4.2). A histogram or preview that fails logs onomaphoto.appwhere Swift'stry?is silent; a failed histogram leaves the bins empty and ready, as Swift's.setLevelsSampleModeis the sheet's write (Swift's sheet setssampleModeand bumpsbrushRevisionitself); it notifies. The commit writes the layer in place and drops shape and text, as the filter's does.beginLevelsleaves out Swift'slevels == niland itsactiveLayeranddocumentguard (canAdjustColorsreads all three);hueSaturationarrives with 9.3,cancelCropwith 10.1, the adjustment layer hooks ofupdateLevels,cancelLevelsandcommitLevelscame with 9.5c. The gates,canAdjustColorsandcanStartProjectOperationreadlevels,selectLayerandselectToolrefuse while it is open, and closing it resumes file requests, Swift'sdidSet; the commit's end resumes them throughsetIsProjectBusy(false), after the edit is gone.commitLevelshands 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 inpreviewImageand in a landing preview (turning it off clears the preview and forgets the run), and in the automatic levels contrast'slow < high(each channel's low lies below its high, so theirs do), neutral's0 < mean < 1and 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'smax(black + 1, …)(normalizedkeeps 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'scommittingcheck (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.LevelsFailureTestsstarves allocations throughAddressSpaceLimitin a binary of its own: a canvas-wide stroke leaves hundreds of MB free inside malloc's arenas, whichmalloc_trimcannot 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/.cpptwin Swift's sheet;Histogram,Gradient,TrackandHandleare private widgets of that file, and the histogram and the tracks follow the session'schanged()themselves. Each handle is a 22 by 20 widget of its own, a child of the sheet placed over its track, as Swift'sForEachmakes 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-pointtriangle.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'sPickerFields: 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.numberformat follows the user's (a German gamma reads2,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 onPaletteChange. The channel picker is named by its label (setBuddy): on Linux Qt names a combo box by its label relation, never byaccessibleName. 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.cppholdssynchronizePanels, Swift'sonChange(of: levels == nil), and since 10.1 the importer and the alerts, becauseContentView.cppruns to 500 lines: aFloatingPanelnamedlevelsPanelshows a new sheet when an edit opens, its close cancelling as Swift'sonClose, and hides when the edit closes. The canvas drawspreviewImagein the layer's place after a filter's; a press in a sample mode issampleLevelsand 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;pickingreads the sample mode, so the eyedropper cursor shows; the application filter's brackets rest.ContentView::acceptsDropneeds no term of its own:canSwitchreadscanStartProjectOperation, which readslevels. The Image menu's Levels… is Ctrl+L and followscanAdjustColors(itshueSaturationterm 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 itsmax(0, …), each standing in for the other (with no bin counted, 0/0), itsmin(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'smax(black + 1, …)and gamma'smin(0.999, …)(normalizeddoes each); either of a track's move and resize events alone (a layout moves and sizes it together); the canvas's!previewbefore the Levels preview (a filter edit and Levels never overlap); passing an unhandled key on (nothing above the canvas takes keys) and the basechangeEvent(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/.cppandDocument/ImageAdjustments.h/.cpptwin Swift's files,UI/CurvesControls.h/.cppSwift's view.AdjustPixels.cwas already compiled for the downsample cache; Curves and Exposure runlevels_applyfromLevelsPixels.c, as Swift's do.CurvesSettings.channelsis a vector of point vectors, as Swift's array, soisValidstill counts four.valuefinds Swift'slastIndex(where:)with a loop; a NaN input takes the first segment at t = 0, as Swift'smax(0, .nan)gives 0.ImageAdjustmentPixels::runhands its body the premultiplied RGBA contextBrushRaster::contextmakes and returns that context.AdjustmentColorkeeps 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), asnormalizedserves the filter's settings since 9.4a and the adjustment's with 9.5;ends, whichapplyreads, gains the filter sheet as a reader with 9.4b and returns anEndsstruct for Swift's tuple.GrainSettings::applytakes an optional seed that replaces the stored one and hands the same image back when the amount is 0. Left out as dead: theisFiniteterms ofAdjustmentColor.isValidand 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).CurvesControlstakes Swift's binding as a value and a change function and shows the settings onsynchronize, 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'sonChange. 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'sInt(…); SwiftUI'smonospacedDigithas 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.numberfields, without group separators, as every field of the twin; leading keepsDouble(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),< 0for<= 0in 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'smax(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 > 1andx < 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.htwins 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) andHueSaturation+Session.cpp(the session extension,canAdjustColorswith it;HueSaturation+Session.hits fragment).ColorRangeandHueSampleModekeep Swift's raw values and case order (allColorRanges,colorRanges,allHueSampleModes);defaultBandis a free function where Swift has a property, andhelpgives the modes' help; their SF Symbols, the eyedropper and its badges, are the sheet's to draw (9.3b).HueSaturationSettingskeeps Swift's dictionaries asstd::maps, so the constructor's entry for its range and the setters' inserts compare as Swift's do; Swift's computedhue,saturation,lightnessandbandare getter and setter pairs.HueSaturationFilteris a namespace;HueResponseandColorstand for Swift's tuples, andcubereturns the floats Swift packs intoData. Core Image'sCIColorCubebecomes 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 isExportError(render), since a worker'sbad_allocwould pass its catch;HueSaturationFailureTestsreaches 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.HueSaturationEditis a class in astd::optional, asLevelsEdit, itsidstanding 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'shueSaturation === 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 setsbrushErrorwhichever edit asked, as Swift'sadjustedPixelsdoes. Known difference: Swift'sbeginHueSaturationresolves 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, asbeginLevelsdoes.commitHueSaturationrefuses 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 twodefers) and names the new asset after the layer; the rebuild drops shape and text.sampledHuereads the composite throughPickerHSB, as Swift's;beginHueTargetingtakes Swift'smax(by:), the first of the strongest claims.setHueSampleModeandsetHueTargetingare the sheet's writes and notify on a change;endHueTargetingchanges nothing observable and stays silent. Swift's beeps are warnings onomaphoto.app. Left out as dead:beginHueSaturation'sdocumentandactiveLayerguard (canAdjustColorsreads 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 (toRGBdraws 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(sobeginFilter),beginLevelsandProjectWorkspace::canSwitchreadhueSaturation;canUseHistory,canStartProjectOperation,selectLayerandselectTooldo not, as in Swift. - The Hue/Saturation sheet (9.3b):
UI/HueSaturationSheet.h/.cpptwin Swift's file,HueSaturationSheetandSpectrumEditor; the editor takes Swift's binding as a value and a change function, asCurvesControlsdoes. The sheet writes through Swift'ssettingsbinding (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-wideQComboBox, resting under Colorize; Swift hides its label, so a hiddenQLabelnames it through the label relation Linux reads (there a combo box's own name is its text). Swift's sliders run freely; the twin's step in hundredths and show the session's value underQSignalBlocker, clamped to their range, so a typed value past it rests the slider at its end without writing back. Each field is aPickerField: a readable, finite number applies when the field lets go of focus (Swift's value binding; Swift'sInt()traps on the rest, which revert), and Return clamps it to the slider's range (Swift'sonSubmit), 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'sunitSuffix. 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 throughEyedropperIconat 14 points with an 8-point disc cut by a plus or a minus (Swift'splus.circle.fillandminus.circle.fill), andhand.point.up.leftas a finger and a palm outlined and turned;HueSaturationSpectrumTestspins each glyph's ink, measured once. The spectrum paints Swift's 72 slices, the after bar throughshiftedHue, 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'snearestHandle); the held handle stays until a press starts afresh, because Qt can lose a release, and Swift'swidth > 0is left out (a press needs width). "Limited to the selection" shows with a selection, never for an adjustment layer's editor (Swift'sadjustmentOriginalterm, 9.5d).ContentView::synchronizePanelsshows the sheet inadjustmentPanel, titled Hue/Saturation, while an edit is open; its close button and Escape cancel. On the canvaspickingcounts 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'smouseDownorder. Targeting showsQt::SizeHorCursor(Swift'sresizeLeftRight) 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'smouseDownsees it: a text draft and a polygonal outline can both be open under Hue/Saturation (Swift'scanAdjustColorsasks for neither). The press, drag and release live inEditorCanvas+Picking.cppbeside 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) followscanAdjustColors, 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 horizontalQSliderexpands by itself),nearestHandle'sfmod(Swift's too: the handles and the pointer lie within one turn) andendHueTargetingon 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/.cpphold Swift's model (Remove Background's kind, its four settings,SubjectRemovalandcommitBackgroundMasksince 9.7);isImageAdjustmentand the list of kinds arrive with the Filter menu (9.4b), their only reader.FilterSettings::normalizedclamps throughImageAdjustmentPixels::clamp, a value that is not finite taking the default.PixelFilter::runhands Curves, Exposure, the Gradient Map and Grain to their settings'apply(Grain in layer pixels,1 / scaleunits, the job's seed), the fill toContentFill::run, and the rest a premultiplied copy, as Swift's contexts: Gaussian Blur isPixelAdjust::gaussianBlurunclamped, clear past the edge as Core Image blurs a finite image; Motion Blur isPixelAdjust::motionBlur,CIMotionBluron 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.candLensPixels.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::prepareis Swift'spreparedandpreparein 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).growForBlurpads by the blur's reach (3σ + 2, or half the streak + 2) throughgrow, 30,000 a side and 100 megapixels at most (ProjectError(tooLarge)), and never shrinks the grid; its rounding up is left out, sincegrowtakes the integral box. The session keepsfilterSettings, 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 isbrushError; 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 leavepreparingset for good).commitFiltercloses a Gradient Map picker with OK first (cancelFilterwith 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'sgradientMaptarget (ColorPickerTarget::highlightsnames the end; the title follows it) opens on the open Gradient Map throughopenGradientMapColorPicker,previewGradientMapColorwrites the working colour into the map throughupdateFilter(silent when nothing changes), andcloseColorPickerwrites OK's colour or puts the original back, the palette untouched. Left out as dead: the opener'scommittingterm (a commit keeps the project busy, whichcanEditPalettereads) andsetGradientMapColor's (a commit closes the picker first); that function's other guards came back with 9.5c.PixelFilter::trimmedthrowsExportError(render)when its crop finds no memory, since Swift'scroppingshares the pixels and cannot fail so; the blurs' row lists (rowNumbers) turnbad_allocintoExportError(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, underAddressSpaceLimit), and inImageAdjustmentTestsSwift's two Image-menu tests and the command half ofcurvesIdentityAndImageCommandPreserveAlpha(Swift'sAdjustmentLayerTestsholds it). Equivalent mutant:>=ingrowForBlur's guard (at equal reach it grows the same grid again). - The filter sheet (9.4b):
UI/FilterSheet.h/.cpptwin Swift's file,FilterSheetandGradientMapControls. The sheet is the content of aFloatingPanelnamedfilterPanel, whichContentView::synchronizePanelsshows with the edit, titled by its kind, and closes with it, its close button and Escape cancelling (Swift'sonChange(of: filterEdit == nil)). The kind's controls are made once, since an edit's kind never changes. Swift'scontrolis 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-pointPickerFieldwith its unit two points after (Swift'sunitSuffix), 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 aQSignalBlocker, 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 aQButtonGroupbeside its label (Swift's segmented picker), Monochromatic aQCheckBox; Lens Correction's note is callout-sized inPlaceholderText; Curves hostsCurvesControlsthrough the settings.GradientMapControlsdraws Swift's gradient bar (20 high, rounded 4, a 35% black rim inside), the ends as the palette'sSwatchButtons (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 callspreviewGradientMapColor(Swift'sonChange), which ignores a palette picker. The controls wait for the sheet's first synchronize, which runs once they are built.settingsandupdatekeep 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) listsallFilterKindsbut 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' inSelectMenuTestsandCompositorMenusTests. Equivalent mutants: Swift's?? .gaussianBlurfor the kind (the panel makes a sheet only for an open edit) and the memory of the picker's colour (previewGradientMapColorwith an unchanged colour changes nothing). - Adjustment layers in the document and the store (9.5a):
Document/LayerAdjustment.h/.cpptwin Swift's file,LayerAdjustment+Session.hits session half.AdjustmentKindkeeps Swift's raw values (rawValue, andadjustmentKind(text)for the manifest) and case order (allAdjustmentKinds);filterKindcame with 9.5c, its reader;symbolnames an SF Symbol, whose glyph the panel draws (9.5d).LayerAdjustmentkeeps Swift's fields: the legacy Hue/Saturation four, the optionalhsvSettings, Levels and Curves, and the optional Exposure, Gradient Map and Grain, whose computed properties are getter and setter pairs;resolvedHSVfalls back on the legacy four.isValidkeeps 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 theisFiniteterms the bounds already refuse and Levels' count (the twin holds four ranges).applyarrives with 9.5b, its reader.ImageLayer.adjustmentjoins 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'sadjustmentis Swift's synthesizedCodable, written by hand inIO/ProjectStore+Adjustment.cppover the readers the manifest shares (ManifestJsoninIO/ProjectStore+Json.h): every key required but the four optionals, which stay out when absent; aColorRange-keyed dictionary is an array of key and value in turn, since Swift'sColorRangedeclares noCodingKeyRepresentable(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 aUInt32. 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::validaterefuses clipping to an adjustment layer, socanLinkMaskrefuses one as a source;canPaintrefuses an adjustment layer's pixels, not its mask.addAdjustmentputs a blank layer named by its kind just above the active one (into an active folder, uncollapsed; Swift'saddBlankLayeralone lifts a layer above the folder's contents), as oneNew … Adjustmentstep, 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'sadjustmentEditingID, 9.5c).updateAdjustmentwrites valid settings alone, with no step of its own, and movesbrushRevision. 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'slegacyHSVStillDecodesAndRenders; the rest arrive with 9.5b and 9.5c), Swift'ssettingsSaveAndOlderAdjustmentsStillOpeninImageAdjustmentTests, and an unbaked copy inProjectCopyTests. - Adjustment layers drawn (9.5b):
LayerAdjustment::applyruns 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 anddrawLiveCompositekeep 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).LiveMaskRenderergains Swift's three hooks as public members:adjustment,adjustmentOpacityandadjustmentClip, which multiplies the layer's own mask into device coverage where Swift clips the context.prepareStackskeeps every layer's mode (Swift'sblendMode); Swift'sadjustment(base) == nilthere is dead and left out, since nothing clips to an adjustment (LiveMaskGraph).drawCompositehands an unclipped adjustment toadjustand draws a clipped one only inside its base's stack, whereadjustruns on the group after its unpremultiplied base, as Swift's.adjustreads the painter's device (aQImage, asLayerRendererrequires; else alogic_error), converted to the kernels'RGBA8888_Premultipliedat a pixel ratio of one, within the pixel budget (past it, a warning onomaphoto.rendering, where Swift'stry?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::compositefor 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 inSourcemode 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 onlyRGBA8888_Premultipliedat 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 isExportError, caught there as a warning; anything else is a bug and is left to crash.ImageExporter::render,drawLiveCompositeand the canvas set the hooks as Swift's three places do;drawLiveCompositegains Swift'sonSurfaceand, when a document holds an adjustment, draws throughAdjustmentSurface, 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'sliveMaskEdit), placed by the layer's transform at the view's scale; itsDisplayStatecounts adjustment layers with their settings, since an undo restores settings without movingbrushRevision. Tests: Swift'sAdjustmentLayerTests(the curves command test moved there fromImageAdjustmentTests, its Swift home),theLiveCompositeDrawsAsTheExport,opacityMixesAndGrainSitsInTheDocument,everyKindAdjustsThroughItsSettingsandaFoldersMaskClipsTheAdjustmentsInsidebeside them,anyImageTargetTakesTheAdjustmentbeside them,anAdjustmentAdjustsWithinItsBudgetAndStackandanAdjustmentTakesAnyImageTargetinLiveMaskRendererTests,adjustmentsShowAsTheExportDoes,anAdjustmentsMaskClipsItOnTheCanvasandgrainFollowsTheViewOnTheCanvasinCanvasDisplayTests. - Editing adjustment layers in the session (9.5c):
Document/AdjustmentEditing.cpptwins Swift's file,AdjustmentEditing+Session.hits session half;adjustmentEditingID(a getter and a setter that resumes file requests and notifies, Swift'sdidSet) andadjustmentOriginal(a getter) sit inEditorSession.h.addAdjustmentasks for the new layer's editor last, as Swift's.beginAdjustmentEditingtakes 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 throughImageExporter::renderin a worker (QtConcurrent::runwith a watcher per call, as each Swift call is a task of its own; the worker catchesExportErroralone, 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, whichbeginLevelsshares), Hue/Saturation withresolvedHSV, or the filter panel offilterKind(declared inLayerAdjustment.h, as Swift's extension) with the four settings; then it keeps the original and opens oneEdit … Adjustmenttransaction. The editors' layer has an id of its own, so the canvas never shows their previews, and none renders:updateLevels,updateHueSaturationandupdateFilterhand the settings topreviewAdjustmentEditingfirst (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 tofinishAdjustmentEditingfirst (a filter's after its Gradient Map picker closes, as in Swift).finishAdjustmentEditingwrites 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 movesbrushRevision. 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 whenfinishAdjustmentEditingis called directly (Swift's tests call it; the app's cancel and commit close the picker first), sosetGradientMapColorkeeps Swift's guards: with no filter open, or another kind, it writes nothing; itscommittingterm is dead, since a commit closes the picker before it begins. Left out as dead: Swift'seditedAdjustmentreturns nil when its kind's edit is gone, which never happens whileadjustmentOriginalis set (the two are set and cleared together, and every path that closes an edit reachesfinishAdjustmentEditingfirst), so the twin reads the edit throughvalue(); the?? .curvesbehindfilterKind, 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 itshueSaturationPending = 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 behindcanEditLayers). 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 onomaphoto.app. A render or an editor that fails clears the request and setsbrushError, as Swift'scatch.canEditLayersandcanStartProjectOperationread the request;canUseHistorydoes 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'sisGroup != truethere 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 itsadjustmentOriginalterm (the original never outlives its kind's edit, which the next terms read). Tests:AdjustmentEditorTests(Swift'ssharedEditorsKeepPixelsDynamicAndSupportCancelfor 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'snewAdjustmentLayersStartFromThePaletteRenderAndEditInThePanelinImageAdjustmentTests, the request as a gate inSessionGateTests; tests that add an adjustment and edit on clear the request, as Swift'sadddoes. - Editing adjustment layers in the window (9.5d):
LayersPaneltwins Swift's.task(id: adjustmentEditingID): on everychanged()(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 aQToolButtonwhose menu (Swift'sMenu,InstantPopup) lists the six kinds by name and callsaddAdjustment, resting whilecanEditLayersdoes not hold. Its glyph and the rows' areLayerIconspaths for Swift's SF Symbols;symbolnames 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), andadjustmentThumbnaildraws 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'slinkable); a double click on a control makes it active and asks for its editor (Swift'srenameClickedLayer), one on the name renames. The Layer menu opens with Swift's New Adjustment Layer submenu (the kinds with an ellipsis, namednew…Adjustment, resting whilecanEditLayersdoes 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'sadjustmentOriginalterms. Tests: Swift'sadjustmentEditorsUseMovableNonmodalPanelsfor every kind inFloatingPanelTests;AdjustmentPanelTests(the footer and its request, the rows and their glyphs, the Layer menu, the sheets' words); the Layer menu's count and names inCompositorMenusTests. - Layer effects in the document and the session (9.6a):
Document/LayerEffects.h/.cpptwin Swift's model: the four effects with Swift's defaults,LayerEffectKindby raw value in Swift's order (allLayerEffectKinds),LayerEffectSelection,LayerEffectswith its helpers. Swift's file runs to 544 lines, so its session extension isLayerEffects+Session.cppwithLayerEffects+Session.h, and its renderer arrives with 9.6b.isValidkeeps Swift's bounds and leaves out theisFiniteterms the bounds already refuse;offsetfalls away from the light, y down. The manifest'seffectsis Swift's synthesizedCodable, written by hand inIO/ProjectStore+Effects.cpp: every key required butenabled, which stays out while absent; the store holds no rule for effects, as Swift's. Swift 1.1 gave the record aneffectskey but itsprojectSnapshotandinstallProjectnever 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 andImageLayer(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, the filter's, Levels' and Hue/Saturation's commits, a floating merge, a copy between projects and both resizers leave them out. The session'seffectsEditing,effectsEditingOriginalandeffectSelectionare getters; 9.6d gave the first two setters, which notify on a change, forContentView's check, and madedropEffectSelectionpublic for the canvas's press (the list's arrive with 9.6e).selectLayer,selectLayersandselectLayerTargetdrop 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.setEffectsfinishes an opacity drag and makes one step of a valid change;changeEffectsstays 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.hdeclaresLayerEffectKindopaquely, sinceLayerEffects.hincludes it. Left out as dead: theisGroupterms ofcanEditEffects,setEffectsandcanCopyEffect(a folder never holds pixels,LayerHierarchy::validatesees to it, soassetsays the same),selectEffect'sselectedLayerIDs = [id](setActiveLayerIDsets it, andselectLayercannot refuse behindcanEditLayers) and its effect picker close (every path that clearseffectsEditingcloses that picker first,ContentView's check since 9.6d included). Swift has no effects tests:LayerEffectsTestsandLayerEffectsSessionTestspin its code, andProjectCopyTeststhe copy. - Layer effects drawn (9.6b):
Rendering/LayerEffectsKerneltwinsMetalLayerEffectsas 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'suchar(x * 255 + 0.5). The spread is Swift's CPUextreme, 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.LayerEffectsKernelTestsreplays 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 whatrenderallows, 100 million. Its buffers are allocated before any worker runs, through one helper, and one that cannot be isExportError(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).LayerEffectsRendererisDocument/LayerEffects+Renderer.h/.cpp, since Swift's file runs past 500 lines:cachedkeeps Swift's cache (the pixels and mask by identity,QImage::cacheKeyfor===, 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 onomaphoto.renderingwhere Swift'stry?is silent, and the layer then draws bare.placed,marginandrenderfollow Swift's; the mask is multiplied in withDestinationIn, smoothly scaled, where CoreGraphics clips to it, and the pixels are copied into the padded grid before the kernel runs.ImageExporter::renderanddrawLiveCompositedraw a layer's effects image throughplacedwith 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 (renderrefuses the same effects and the cache's catch returns none; the term only spares the warning). - Layer effects on the canvas (9.6c):
Rendering/EffectsPreviewCachetwins Swift's class, aQObjectthe session holds aseffectsPreviews, Swift's@ObservationIgnoredmember. Swift's serial queue is aQThreadPoolof one thread; a request waits 60 ms on a single-shot timer (precise below two seconds) and then joins the pool; Swift's lockedcancelledis a shared atomic flag; the result is posted back and lands only while its request is still the layer's, Swift's id check.Requestholds the asset, so a painted layer flattens on the worker.matches, the keptprevious,seed,rendered,prepareand the shared budget (64 MiB of output, 32 to 1536 a side) follow Swift's. A reduced preview scales each source throughQImage::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 onomaphoto.renderingand lands none, as Swift'stry?. 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 aQPointer, 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/LayerEffectsSurfacetwins Swift's class:makefor its failableinit(80 million pixels at most, a warning past it or on a failed allocation),matches,update(tiles taken by origin andcacheKey, Swift'sObjectIdentifier),composeandwindow. Swift's CPU fallback (filland 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 equalsLayerEffectsRenderer::renderbyte for byte, and every later pass a fresh render of the painted pixels (LayerEffectsSurfaceTests). Left out as dead: the reach'sisEnabledterms (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'sceiland the region'sintegral(every box is made whole where it is used, and tiles lie on whole pixels).- The canvas's effects (9.6c):
DisplayState::Layercarrieseffects,drawLayerscallsprepare, andsynchronizeDisplayhands a finished stroke's surface to the cache as a seed first (handOnStrokeSurface, Swift's opening lines).drawOwnkeeps 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 (EffectsCanvasTestspins 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 theirconst: they update the stroke surface and hand the cache a repaint.drawStrokemoved toEditorCanvas+Effects.cppbesidestrokeSurfaceandhandOnStrokeSurface, keepingEditorCanvas.cppunder 500 lines.Distort.cppgainsDistortEffectsCache, bothdistortedEffects(constwith a mutable cache, asdistortPreview; a warp that throws leaves the effects off, Swift'stry?) andcommitDistort's seed, the cache cleared after the step as Swift'sdeferclears it. Swift has no tests for any of this:EffectsPreviewCacheTests,LayerEffectsSurfaceTestsandEffectsCanvasTestspin its code. Equivalent mutants: the pool's thread count (a result lands by its request's id, whatever the order), the destructor'sclearand 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),!previousbefore 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 readslround(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).EffectsFailureTestsstarves a stroke's first pass throughAddressSpaceLimit, 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 forAddressSpaceLimitto reach a worker's arena. - Layer effects' panel (9.6d):
UI/EffectsSheet.h/.cpptwin 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 checkableQToolButtons in an exclusiveQButtonGroup(Swift's segmented picker; its hidden label is the box's accessible name), the colour as the Type bar'sSwatchButton(36 by 18, radius 3, a half-black rim,<Kind> colorits tooltip and accessible name, black while the effect is missing) openingopenEffectColorPicker, Swift'ssliderrows (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-alignedPickerFieldwith 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.numberformat 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'schangeEffects. On every change of the picker's colour the sheet callspreviewEffectColor(Swift'sonChange), and it repaints its swatch on every change. While the layer lacks the effect the controls hide, as Swift'sif let effect.ContentView::synchronizePanelsfirst runs Swift's layers check (an edited effect gone from its layer, or the layer gone, closes an effect picker without its colour and clearseffectsEditingandeffectsEditingOriginal; a palette's picker stays), then shows the sheet ineffectsPanel, titled by the kind, whenevereffectsEditingchanges (a new choice, a new sheet), its close button and Escape cancelling, and closes it when the choice clears. The footer's effects button (sparklesinLayerIcons: three four-pointed stars) is an instant-popup menu of the four kinds, each with an ellipsis, resting whilecanEditEffectsdoes not hold. While an effect is chosen (selectedEffect, so a choice an undo took away names the layer again) the trash saysDelete selected effectand the Layer menu's DeleteDelete <Kind>. The canvas's left press lets go of a chosen effect before any guard, Swift's first line ofmouseDown; other buttons keep it. Swift has no tests for the window's effects:EffectsPanelTestspins them, andLayerEffectsSessionTeststhe new writers' signals. Equivalent mutants: the fields'OmitGroupSeparator(no range reaches a thousand) and OK's explicitsetDefault(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 inNativeLayerList.h, its code inNativeLayerList+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, asnoteHeightOfRowschanges 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,eyeoreye.slashinPlaceholderText, namedHide <Kind>orShow <Kind>) toggles the effect in one step and rests whilecanEditLayersdoes not hold; the name (11 pixels,WindowText,PlaceholderTextwhile 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 showsHighlightat 30%, and while one is chosen the cell's selection hides, as Swift's table selection does. Presses follow Swift'smouseDownorder: Alt without Ctrl on the strip (the bottom 10 points of the taller row, over its last effect; Swift'smin(10, height / 3)is dead, rows being at least 52) clips throughclickRow; on an effect, Alt without Ctrl arms a copy while layers can be edited, the release choosing the effect and a move paststartDragDistancewith Alt still held starting aQDragofUUID:KindundereffectTypewith 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'sclickCount > 1); the eye's Alt presses are the row's, ignored in its filter so they go on to it (Swift'shitTest). An effect dropped from the same list (Swift compares the source's session) lands on the row under the pointer, filled, as a copy, wherecanCopyEffectallows. A left press below every row lets go of the chosen effect and, without Ctrl or Shift, of every layer, as Swift's table withallowsEmptySelectiondoes (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:LayerEffectRowTestspins them, withQDrag::execdefined in its binary as inLayerDragGestureTests. - Remove Background (9.7): Apple's Vision has no Linux twin, so
Document/SubjectRemovalruns U²-Net (Qin et al., Apache-2.0, its text inlicenses/U-2-Net.txt) on ONNX Runtime (MIT), as rembg converted it (MIT):u2net.onnx, 175,997,641 bytes, SHA-2568d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491, the MD5 rembg pins. Chosen overu2netp(poorer edges),silueta(no clear source) and IS-Net'sisnet-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 withQStandardPaths::locate(GenericDataLocation, "omaphoto/u2net.onnx"), so a package installs it asshare/omaphoto/u2net.onnxand the dev image carries it in/usr/local/share.Dockerfile.devfetches 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 tolib64, where the config looks for it; CMake linksonnxruntime::onnxruntime. OneOrt::Sessionserves the process, loaded on first use; a load that throws leaves it unloaded, so the next use tries again. Every model step passes throughvision, where ONNX Runtime's exceptions becomeSubjectRemovalError(model)and a failed allocation (the input buffer, the runtime wrapper's own vectors) becomesExportError(render), so none passes a worker's catch.SubjectRemovalErrortwinsSubjectRemoval.Failure:noSubjectwith Swift's words, and amodelkind 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'snoSubject(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 bycacheKey, Swift'sMaskCache.Document/GuidedMattetwins Swift's file:boxkeeps Swift's float sums, the column pass running a row at a time;filterfrees what it no longer needs;levelsscales 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 throughqGray;refinedraws back up withSmoothTransformation, and the model's mask is enlarged through premultiplied RGBA too.refineddoes Core Image's Shift Edge (a Gaussian with the edge repeated throughPixelAdjust::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 isExportError(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 alogic_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 oneRemove Backgroundstep 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'sif. Equivalent mutants: the cache's key (only the time differs) and the commit worker's catch ofSubjectRemovalError(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'svisionRequestRunsOnAnImageamong 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, underAddressSpaceLimit, glibc's mmap threshold fixed so large requests map memory the limit counts),RemoveBackgroundTests, the sheet inFilterControlsTests, the kind inFilterModelTestsand both menu tests. - Crop (10.1):
Document/Crop.h/.cpptwin Swift's file,Crop+Session.hits session half.CropGeometryis a namespace;CropDrag::Modeis a kind and an index, asTransformDrag::Modetwins an enum with payloads;CropSnap'snearestand Swift's localshiftare file helpers;cropSnapTargetsreturnsSnapGuides, Swift's tuple.validandcommitCropread the frame normalised, asCGRect'swidthandminXread it standardised;valid'sisFiniteterms are left out, since the ranges refuse NaN and infinities.cropRect,cropRatioChoiceandcropErrorare getters and setters that notify on a change.commitCroptakes a completion callback, posted on every path; the snapshot is taken and the worker started before anything changes, the resize runs throughQtConcurrent::runand aQFutureWatcherwith the project busy, the worker catchingProjectErrorandExportError(CanvasResizer::resizeturns astd::bad_allocof its records and maps intoExportError(render), as 9.7'svisiondoes), and the result is moved out (takeResult) and installed throughapplyDocumentSize, oneCropstep; memory that runs out while the snapshot is taken or the result lands setscropErrorto 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 setscropErrorto the error's text and keeps it, and the project is freed after either, Swift'sdefer. Hooks:selectToolcancels on a change of tool and opens the Crop tool on the whole canvas, Free, when no frame is drawn;restore,beginTransform,importImages,beginLevels,beginFilterandProjectController::begincancel;installProjectandclearProjectdrop the frame and announce it with their own signal;canEditLayersreadscropRect,canUseHistorydoes not, as in Swift. The canvas's gestures live inEditorCanvas+Crop.cpp. A press on a grip resizes (the first region holding the point, corners first, Swift'sfirst(where:)), one inside a drawn frame that is not the whole canvas moves it, any other drops the frame and draws anew;CGRect.containsleaves out the far edges, so the file's owncontainsdoes too, whereQRectF::containstakes 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 itsif let cropSnapare 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'smouseDragged; a move without the left button ends it, since Qt can lose a release.endCropDragis Swift'scropDrag = nilwith itsdidSet: 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 inm_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 rightSizeFDiagCursor, top right and bottom leftSizeBDiagCursor, top and bottomSizeVerCursor, left and rightSizeHorCursor; Qt's shapes say no inward or outward), elsewhere the crosshair.TransformOverlaygainscropViewRect,cropResizeRegions(aCropRegionstruct for Swift's tuple) anddrawCrop: the canvas outside the frame under 60% black (aQPainterPath'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'scropDraggingRedrawsOnlyTheOverlay: Qt has no layers, so the union of the frames repaints, pixels included, never the canvas. Under the Crop toolrenderBoundstakes the frame's union, so a frame past the canvas shows the checkerboard it would take in and redraws the canvas, as in Swift.CropControlsis the tool'sToolHeaderBar: its row spaced 14 as Swift'sHStack, Swift's labelled picker a "Ratio"QLabel(its combo box's buddy) and aQComboBoxsharing 170 points, a choice that changes the ratio callingchangeCropRatio(Swift'sonChange), the frame's size as whole pixels (Swift'sInt, truncated) grouped through the English locale like the status bar (monospacedDigithas no Qt 6.4 twin), and Cancel and Apply Crop resting without a frame; the bar rests while busy shows or without a document.ContentViewshows it for the Crop tool and the "Couldn’t crop" alert overcropError. Left out as dead:renderBounds' tool test (a frame lives under the Crop tool alone: every other way to another tool cancels it or askscanEditLayers, which a frame refuses) and the strips'max(0, …)incropResizeRegions(a strip it would clip lies under the corners' regions, which take the point first). Equivalent mutants:containson the visible frame for the drawn one (while a frame is drawn they are one), the tolerance'smax(…, 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'ssetBrush(Qt::NoBrush)(the painter arrives with none) andtakeResultforresult()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'scropDraggingRedrawsOnlyTheOverlay; 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 whosemainshares the main heap among threads,M_ARENA_MAX1 before any thread starts, and maps large asks, so a worker's allocations meetAddressSpaceLimit: 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 inContentViewTests. - Canvas and image size (10.2):
flipCanvassits inLayerFlip.cpp, as Swift's extension: aftercommitTransformandcancelCropit askscanEditLayers, mirrors every layer's transform and every placed mask across the canvas's middle and the selection's outline through aQTransform(the path keeps its fill rule), oneFlip Canvas HorizontalorFlip Canvas Verticalstep; Swift'sfinishOpacityEditis left out, sincecommitTransformhas run it.UI/CanvasSizeSheetis 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'sByteCountFormattercounts it for memory (binary units: bytes, whole KB, MB to one place, GB to two, trailing zeros dropped;UI/ByteCountssince 10.3), aMiddivider, Units as a labelledQComboBox, Width and Height asPickerFields (a readable number applies on Return or on leaving, other text goes back, Swift's value binding; printed as Swift's.numberto 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'sonChange), the New line or the invalid note inBrightText, the anchor grid (nine 25-point buttons in an exclusiveQButtonGroup, each a circle filled in the accent when chosen for Swift'scircleandcircle.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'sColorPickeras a label and a 36 by 18SwatchButtonthat opens aQColorDialogwithout 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/ImageSizeSheetis 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'sonChange), 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'scanvasSizeandimageSizelive inIO/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 orbegin, as Swift's guards; the sheet is a window-modalQDialogover the window (sheet, Swift'sbeginSheet), whose Escape and close button cancel; the project stays busy until the sheet answers and the resize lands, or its alert is dismissed, Swift'sdefer.resizeProjecttakes the snapshot and starts the worker before anything changes, the worker catchingstd::runtime_error(both of the resizers' errors), and moves the result out and lands it throughapplyDocumentSizeorapplyImageSize(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 bycanEditLayers. 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,mainasCropFailureTests': the image resizer, the snapshot, the worker and the landing each run out), the canvas flips inLayerFlipTests, the entries inCompositorMenusTests. - Exports (10.3):
IO/ProjectController+Export.cppholdsexportPNGandexportJPEG, each with a completion callback.exportPNGasks for a document andbegin(no window: its panel then stands alone), takes the snapshot, opens the panel and renders, encodes and writes in one worker (exportWork, which catchesstd::runtime_error, the exporter's errors and the write's);exportJPEGalso needs the window, renders in a worker (catchingExportError), showsJPEGExportSheetin the window-modalsheetof 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'sdefer; a snapshot that finds no memory is explained as the render error, andImageExporter::renderandjpegturnstd::bad_allocintoExportError(render)at their doors (function-try-blocks), which also serves 9.5c's editor render; the PNG snapshot's copy for the worker andexportWork'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 andshowError's are moved, never copied, since the PNG flow's completion holds the snapshot.exportPanelis Swift'sNSSavePanelfor one type: aQFileDialogtitled Export PNG or Export JPEG with the type's name filter and default suffix, suggesting the project's name (ProjectTab::nameWithoutSuffix, Swift'sdeletingPathExtension) or Untitled with.pngor.jpg; a chosen name without one of the type's suffixes (png;jpeg,jpg,jpe; any case) gains the preferred one (png, andjpeg,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::jpegtakes Swift's three cancellation points as an optionalcancelledtest, throwingCancellationError(Swift's type) before the flatten, before the encoding and before the preview.UI/JPEGExportSheettwins 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 busyQProgressBar), Quality as a slider in hundredths with its percent (lround, Swift'srounded()) 45 points wide and right-aligned, the matte as a label and a 36 by 18SwatchButtonthat opens aQColorDialogwithout alpha (the same colour asks nothing, as Swift'sonChange), the raster's size grouped with "px · sRGB", then the error inBrightText, 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)isrequest: it sets the last request's flag, clears the error and restartsjpegWait, a 200 ms single-shotPreciseTimer(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 inQSettings; a stored value counts when it reads as a finite number, clamped to 0–1.UI/ByteCountsis Linux-only: Foundation'sByteCountFormatterin 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 fromDialogDesk, 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::aroundAcceptsets 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'smalloc_trim(0)is an equivalent mutant, since no free top chunk serves a 1,532 KB mapping),ByteCountsTests, the cancellation points inJPEGExportTests, the shortcuts and the count inCompositorMenusTests. 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_errorterms 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/ImageFileDroptwins Swift's enum as a namespace.importProviderstakes the drop'sQMimeData, 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 toDropped-<UUID>in the temporary folder, as Swift'stemporaryFilecopies a promised file, the format chosen in Swift's order (PNG, JPEG, HEIC, TIFF, then anyimage/type) and the suffix from Qt's MIME database. The files go to the workspace'sreceiveor, without a workspace, to the session'simportImages; 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'sprojects:parameter has no caller and is left out.holdsImagesis Swift's drop types but the layer row's: a local file or a picture.receiveProviderscopies layer rows as before and hands anything else toimportProvidersone 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, sinceQFilebuffers) is removed and counts as unreadable; a failed flush failsQFile::removetoo, so the copy is closed and unlinked by path. An open alert shows its message as it stands (showAlertupdates 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'svalidateDrop. Tests:ImageFileDropTests. - Slider snapping (10.5):
UI/SliderSnaptwins Swift's enum as a namespace. Swift swapsNSSliderCell.startTrackingso that a press on the track sets the value whose knob is centred there before tracking begins; the twin'sinstallputs one application event filter in place, once, as Swift's lazy static swaps once: a left press on an enabled horizontalQSliderwith a range, outside its knob and with travel for it, sets the slider down, then sets the value under the press (QStyle::sliderValueFromPositionover 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 asinitStyleOptiondoes. 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.maininstalls it after the theme. Tests:SliderSnapTests(Swift'sclickingTheTrackSnapsTheKnobAndStillEditsTheValue: 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 inmainis 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):
DocumentSelectiongainsfeather, the fade's reach in document pixels. Its coverage is filled antialiased and blurred byPixelAdjust::gaussianBlurat sigma feather / 2 with the edge repeated, Core Image'sclampedToExtent;coverageBoundsgrows the outline's box byceil(2 × feather), and the clip, the copy region and a mask made from the selection (now drawn throughPixelAdjust::coveragefrom 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);selectionAmountOperationis a getter and a setter that resumes file requests, read first bycanEditLayers,canUseHistoryandcanStartProjectOperation.SelectionAmountSheet(inLassoControls, Swift's file) is Swift's sheet in aFloatingPanelnamedselectionAmountPanel, titled by the operation; its note keeps its room while hidden, as SwiftUI's opacity;QString::toInttrims spaces as Swift'strimmingCharactersdoes. The selection bar gains Feather and its amount (1–250). Select ▸ Subject (Ctrl+Alt+A) draws the live composite, runsSubjectRemoval::subjectMaskin a worker with the project busy (a completion callback, as the other jobs), and on the same document appliesMaskTracing::whitePixels(at least half gray) as the selection, oneSelect Subjectstep, or setsbrushError;canSelectSubjectleaves out Swift's document and busy terms, whichcanEditLayersreads. Soft Light joinsLayerBlendModeafter Overlay as QPainter'sCompositionMode_SoftLight, which follows the PDF formulaBlendModeTestsholds.TypeFontPickeris 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, andsetTextDraftresumes file requests only when a draft ends, as Swift 1.1.3's. Swift'sGeometrycache, which skips AppKit's frame work, has no twin: the canvas draws the editor. Equivalent mutants: the editor's box branch (textBoxSizereturns the box),setTextDraft's ended test (resumeFileRequestsaskscanStartProjectOperation, 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(inLayerGroups.h) walks up to 64 folders, a lookup standing for Swift's closure;ImageLayer::effectiveOpacity,ProjectLayerRecord::effectiveOpacity(declared inProjectStore.h, defined inLayerGroups.cpp, as Swift's extension) andCanvasDocument::effectiveOpacitiestwin Swift's, the last refusing a repeated id with alogic_errorwhere Swift's dictionary traps. Every drawing path takes it: the export,drawLiveComposite, the canvas (its every draw, adjustments included, andDisplayState, so a folder's change redraws) and the live-mask baker's sources, whose records are pointers, so it callsLayerOpacity::effectiveitself.canEditOpacityis one layer, a folder included;canEditAppearancestays for blending, soLayerAppearanceControlsrests withcanEditOpacityand its Blend caption and picker withcanEditAppearance;setSelectedLayersOpacitydims 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'sguideskey 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 issnappingEnabled, a getter and a setter that clears the guides and notifies (Swift'sdidSet);snappedMoveand 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 byapply;QSliderpins 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'scontentShapehas 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'sactiveLayer()term (one selected layer is the active one).drawPixelGridmoved toEditorCanvas+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'saDimmedFolderSavesAndReopens), folder duplication inLayerDuplicateTests(moved out ofEditorSessionTestsat 500 lines),TransformTestsandLayerDragTests, the controls inLayerAppearanceControlsTests, Snap inTransformSnapTests,CropCanvasTestsandViewMenuTests(the View menu tests, moved out ofCompositorMenusTests), the ranges and swatch inEffectsPanelTests. - Keyboard shortcuts (12.2b, Swift 1.1.6):
UI/KeyboardShortcuts.h/.cpptwin Swift's file,KeyboardShortcuts+Sheet.cppits sheet and recorder.ShortcutChordkeeps 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 areQKeySequence's native text (Ctrl+Alt+K), where Swift prints symbols.ShortcutDefinition::allis Swift's list less Hide Compositor, which has no twin;problemkeeps Swift's rules less its three chords reserved by macOS (the desktop takes its own before the app does).ShortcutSettingskeeps the overrides inQSettingsunder Swift'skeyboardShortcuts.v1, as Swift's JSON, a stored set that failsproblemignored with a warning onomaphoto.appwhere Swift is silent;changed()stands for its observation, andreloadreads the store again, as a launch does. The menus remember each entry's own key in a property and takemenuof it on every change.canvasEventandtextEventhand back the press to handle, or none to swallow: the canvas (its key handling moved toEditorCanvas+Keys.cpp, the navigation file at 500 lines), the layer list and the window's key filter readcanvasEvent, open texttextEvent; a translated press the canvas or list leaves passes the original on, assuper.keyDowndoes. Swift'spanPhysicalKeyism_panKey: the key whose release ends a remapped pan.NativeShortcutisconfiguredNativeShortcut: aQShortcutover the sheet and its children clicks its button when that shows (a disabled button refusesclickby 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 inBrightTextat 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'sperformKeyEquivalent: aQt::Toolpanel keeps its parent window's menu shortcuts live, andQWidget::eventwould move the focus on Tab. It opens from the Edit menu, after Paste and a separator, in thekeyboardShortcutspanelCompositorMenusholds. Equivalent mutants:menu's and the Shift branch'sisMenuterms (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/.cpptwin Swift'sGuides.swift:CanvasGuide(its axis by Swift's raw values,rawValueandguideAxis),LayoutGrid(a namespace),GuideDrag, and the session extension, declared inGuides+Session.hwith 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, andguideDrag), whose state stays inEditorSession.h;guideHitDistanceandguideColorare Swift's statics.finishGuideDragtakesremovingwhere Swift names itdelete.canEditGuidesleaves out Swift's_ = showsBusy, which only registers an observation. 1.1.7'ssnapEnabled(Snap To's master switch, read byalignmentSnapTargetsandsnappedGuidePosition) stands beside 1.1.6'ssnappingEnabled(read bysnappedMoveand the crop drag), as it does in Swift through 1.3.3, with a View entry each.transformSnapTargetsandcropSnapTargetsarealignmentSnapTargetswith and without centres, the canvas's middle after its edges and a layer's between its edges, Swift's order;TransformSnap::box(inCrop.cpp) is the upright box both files read. The document holdsguides, equality and undo included; the manifest's optionalguidesis Swift'sCodable(an array ofid,axisandposition, 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).installProjectandclearProjectdrop a drag;CanvasDocument(snapshot)loads the guides, soapplyDocumentSizeandapplyImageSizekeep 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'sisNewtest on removal and itsoriginal != positiontest (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 inTransformSnapTests. - Rulers, and guides on the canvas (12.3b, Swift 1.1.7):
UI/CanvasRulers.h/.cpptwin Swift's file:CanvasRuler::thickness,CanvasRulerCornerandCanvasRulerView(a widget per strip, repainting on everychanged(), where Swift's canvas asks each ruler to redraw), with Swift'smajorStepandlabel. 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.ContentViewframes the canvas slot in a grid with the corner and the two strips, shown whileshowsRulersholds and a document is open.TransformOverlaydraws 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'sdraw; 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 inEditorCanvas+Guides.cpp: with the Move tool a press on a guide (canEditGuidesandhitGuide) 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'scanEditGuidestest (beginGuideCreationasks it again). Tests:GuideCanvasTests(Swift'smoveToolDragsAGuideAndRulerDropDeletesItandrulerStepUsesNicePixelIntervals; the drawing, repaints, cursors, keys, lost releases, the rulers' layout, ticks, repaints, focus and drags), the canvas's place inContentViewTests. - The Magic tool's Object mode (12.4, Swift 1.1.8):
Document/ObjectSelection.h/.cpptwin Swift's file,ObjectSelection+Session.hits session half;WandMode(Swift's raw values,allWandModes) sits inSelection.h. Vision's foreground instances have no Linux twin (the user's decision):SubjectRemoval::foregroundhands back U²-Net's mask at the image's size (the Remove Background model and cache, 9.7), whichselectscales 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 byGuidedMatte::refine(radius 5) on the image, for Swift's edge-preserving upsample, then cut at half;adjustederodes or dilates a pixel ring a step (eight neighbours, ten steps at most, the image's edge counting as inside, Swift'serodedanddilated),MagicWand::outlinetraces it andsmoothedrounds 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). Abad_allocisExportError(render); Swift'sunsupportedhas no twin. The worker catchesSubjectRemovalError,MagicWandError(the trace's refusals) andExportError, each landing inbrushErroras Swift'scatch.selectionSampleis the wand's sample made public as Swift's;selectObjectruns through the wand's watcher and landing (Wandingcarries 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.pressWandKeychooses 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 (SelectionAmountFieldtakes 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 runsselectObjectahead of the colour wand, a click inside the selection picks afresh, the cursor is the crosshair withSelectionIcon::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'scancelLasso(Magic never holds a draft:beginLassorefuses it andselectToolcancels one),selectObject's busy term (canEditSelectionreads it) and its clamp of the offset (adjustedclamps), and a null check of the enlarged cells (GuidedMatte::refinechecks 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 callingselectTool(pressWandKeyis 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'sMagicWandError: 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 inLassoControlsTests, the icon sheet, the rail and hints inContentViewTestsandContentViewStatusTests. - Photoshop import (12.5a, Swift 1.2.0):
src/IO/PSD/twins Swift'sIO/PSDfolder.PSDTypesholdsPSDError(Swift's five cases and words),PSDConversion,PSDRecord,PSDDocumentandfromPSD(Swift'sLayerBlendMode.fromPSD); Swift'sPSDLayerKind.otheris never assigned, so the case and its note are left out.PSDChannelCoderdecodes raw and PackBits planes (a no-op run skipped, every overruntruncated) and buildsRGBA8888_Premultipliedpixels tagged sRGB, rounding as Swift's(c × a + 127) / 255, andGrayscale8masks; a surface that cannot be allocated isExportError(render)throughBrushRaster::context, where Swift saystruncated.PSDReaderreads Swift's fields in Swift's order over aCursorwhose bounds check cannot overflow (an 8B64 length nearqint64's top reads as truncated, where Swift'soffset + counttraps), maps the file where it can and reads it where it cannot (a pipe, a mount withoutmmap), asmappedIfSafedoes (a file that cannot be opened or read isunreadable, where Swift passes on the Cocoa error), logs every refusal onomaphoto.ioand rethrows it. Pascal names are Mac OS Roman through a table of the 128 high code points taken from Python'smac_roman(Qt decodes it only through ICU); keys are ASCII or nothing, as Swift's.ascii.PSDAdjustments::parse(inPSDReader+Adjustments.cpp) reads Levels, Curves and Hue/Saturation as Swift does.PSDVectordraws vector masks withQPainterPath(tightboundingRect, a path empty only without elements as aCGPath), fills by nonzero winding and strokes withQt::SvgMiterJoinat 10, SVG's miter rule being CoreGraphics' (probed: kept to a ratio of 9.57, bevelled from 10.43;Qt::MiterJoinmeasures from the join point and clips instead);integralstays in doubles, so a wild box reaches the size check instead of an overflowingQRect.PSDDocumentBuildermakes the layers and conversions; a shape's identity is its asset's.ImageImporter::loadPhotoshopandphotoshopAssetsrun on the import's worker,makeImporton the UI thread. In the session (EditorSession+Imports.cpp) a file whose first bytes are8BPStakes the Photoshop path:beginPSDReadingshows the sheet while aQFutureWatcherreads it,finishPSDReadingfills it or takes it away,finishConversionanswers 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 throughinsertPhotoshop(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 oneImport Photoshop Filestep. Fixes beyond Swift:insertPhotoshopvalidates 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'showsConversionSheetterms and itsdidSet(the sheet shows only while importing, which the gates refuse already), Swift's test hookconfirmConversions,confirmPSDConversionsandNativeLayerList.moveLayer(no caller), the request'sid, the builder's folder and adjustment checks on a clipping base (only pixel layers become bases), the reader'slayerSectionEnd, 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 aroundmakeImport(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/PSDConversionSheetis Swift's sheet in a window-modalQDialog(conversionSheet) thatContentViewshows 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 asDropped.<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 andskip'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), thereservecalls,painter.end()before the raster returns, the drop copy'sclose()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:PSDRoundTripTestsandPSDVectorTests(Swift's, throughtests/PSDFixture.h, Swift's writer, andPSDVectorFixtures.h),PSDReaderTestsandPSDReaderLayerTests(the reader's edges, every cut before the last layer byte, budgets and vector layers read whole, built byPSDReaderFixtures.handPSDVectorBuilders.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):
OuterGlowEffectjoins 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'souterGlowis Swift'sCodable(size,opacity, the colour,enabledonly while set).LayerEffectsKernelblurs 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 CPUouterGlowCoverageserves 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'sdbe82b0). 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.BrushControlsshows Smoothing for the Brush alone: a 100-point slider in tenths and a 42-pointSelectionAmountFieldof 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.moveLayerhas no caller; the spelling touches shader comments), but for two follow-ups of90ee29fand7c4512a: 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 inLayerEffectsKernelTests' shader replay,LayerEffectsSurfaceTests' rows andEffectsPreviewCacheTests' scaling, the kinds, manifest and menu in their files;BrushSmoothingTestsand the bar inBrushControlsTests. - Camera RAW (12.6a, Swift 1.2.1): Core Image's RAW filter has no Linux twin, so
IO/RawImporterdevelops through LibRaw (the user's decision), its thread-safelibraw_rfound by pkg-config;Dockerfile.devinstallslibraw-dev, the PKGBUILD and the flake depend onlibraw, the distro images carry it. A RAW is any file whose extension names a type inheritingimage/x-dcrawin the MIME database (matches,isRawTypefor a dropped picture,globsfor the importer's filter), Swift'sUTType.rawImage.RawDevelopSettingskeeps Swift's fields,isAsShotandreset;RawDevelopRequestis Swift's tuple. White balance:planckianis Kim et al.'s cubic (1667–25000 K),whitePointmoves off the locus along its greener uv normal by tint ÷ 3000,temperatureOfinverts it by a mired search and golden sections: Adobe's scale and sign (the DNG SDK'skTintScale), 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.asShotreads the camera's multipliers (cam_mul, else dcraw's daylightpre_mul) and turns the neutral into xy through LibRaw'srgb_cam(a DNG'scam_xyzstays zero) withpre_mulput back;developworks back from the chosen white to camera multipliers (user_mul, the second green the first's), reading the camera'spre_mul,rgb_camand size from unpack's copy (rawdata), since a process overwritescolorwith its own multipliers and a half-size one halvessizes; 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 isImageImportError(whiteBalance), a kind of Linux's own with words of its own, sinceCIRAWFiltertakes 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_thr0, so a frame near white keeps its level), clipped at white in camera space (highlight0, 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 opaqueRGBA8888_Premultipliedtagged sRGB, oriented by LibRaw. Only three-colour sensors develop; a four-colour one isunsupported(dcraw's matrices hold no fourth to invert). A preview past its limit decodes at half size, then fits smoothly.Queueis 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,releaseletting 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 areunreadable, logged with its message onomaphoto.io; a surface that cannot be made isExportError(render). In the session (EditorSession+Imports.cpp) a RAW goes before the Photoshop check:pixelSize(unreadable when it fails) and Swift's budget (tooLargepast 30,000 a side or the pixels left), thendevelopRawshows the sheet with the camera's settings (the defaults when they cannot be read) and keeps the answer,finishRawDevelopclears 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 catchesImageImportErrorandExportError, into a layer named for the file.rawDevelopalone stands for Swift'srawDevelopandshowsRawDevelop, which are set and cleared together; the latter'sdidSetis dead (the sheet shows only while importing, which holds file requests anyway), and so is the test hookconfirmRawDevelop.UI/RawDevelopSheetis Swift's sheet in a window-modalQDialog(rawDevelopSheet) thatContentViewshows 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 inPlaceholderText: 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'stask(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_onlyfails 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 inDNGFixture.h, the drop order inImageFileDropTests, the filter inContentViewTests, the entry inDesktopIntegrationTests.tests/Tests.cmakeholds the test binaries, keepingCMakeLists.txtunder 500 lines. - Middle-button pan, tool defaults, Auto Select and the icon (12.6b, Swift 1.2.1):
Document/ToolDefaultstwins Swift's enum as a namespace:boolean(Swift'sbool, a C++ keyword) andsetkeep Auto Select, the transform controls, the pixel grid, the layout grid, guides, rulers, Snap, the four Snap To switches and Lock Guides inQSettingsunder Swift'stool.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'sdidSetsees those writes. Swift returns the defaults under XCTest; Qt has no such flag, and most test binaries run outsideQStandardPaths' test mode, so the twin keeps the defaults untilToolDefaults::enable(), whichmaincalls first (not under test, as the theme's construction is not). The settings file keeps text, so a stored value reads astrueorfalse; anything else is ignored with a warning onomaphoto.app, where Swift'sas? Boolis silent. The canvas's middle button (Swift'sotherMouseDownfor 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'smouseDown, the press takes no focus.transformPressLayergains 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);abovereads both layers through checked lookups, since the pointer's layer and the visible active one are both drawn, where Swift guards each index. Swift'sunderPointer != active.idis left out as dead (aboverefuses a layer against itself); equivalent mutant:>=inabove(only the active layer equals itself, andselectLayerof the active layer changes nothing). The reshaped app icon (45ab8fd) replacespackaging/icons. Tests:ToolDefaultsTests(the defaults until enabled, every toggle across sessions and as text, guides shown by a guide, a strange value),theMiddleButtonPansFromAnyToolinCanvasNavigationTests, Swift'sautoSelectPicksForegroundLayerStackedOnSelectedBackgroundand a mask box past its layer inTransformPressTests, whose older Auto Select test now expects the layer above. - Photoshop's blend modes (12.7a, Swift 1.2.2):
LayerBlendModetakes Swift's order, Photoshop's, andallLayerBlendModesfollows 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.blendGroupstands for Swift's staticgroups: each group's modes sit together in the enum, so the group is the last first member a mode reaches.BlendModePickerdraws 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'sLiveMaskRenderercomposites a clipping stack's group and an adjustment's result throughcgMode, which is Normal for the eight (v1.2.2'sprepareStacks,drawCompositeandadjust, unchanged through 1.3.3), so a base or an adjustment in one of them draws as Normal there; the twin hands every mode toLayerRenderer::composite, which blends the eight by hand, as Photoshop composites a clipping group with its base's mode (ClippingStackTests::aStackBlendsOnceWithItsBasesMode,AdjustmentLayerTests::adjustmentBlendAndSoftMaskPreserveCoverage). Swift'scoreImageFilterhas no twin:needsSurfacenames the modes. Equivalent mutants:<=against<at one half in Vivid and Pin Light (both branches give the backdrop there). Tests: the formulas inBlendModeTests, the menu's groups and keys inBlendModePickerTests, the order inLayerAppearanceTests,BlendShortcutTests,EditorCanvasTestsandProjectManifestTests, the keys inPSDRoundTripTestsandPSDImportTests, the canvas against the export inCanvasDisplayTests::photoshopsOwnModesShowAsTheExportDoes. - Black & White, Color Balance and Invert (12.7b, Swift 1.2.2):
kernels/AdjustPixels.c/.hare v1.2.2's, withadjust_black_whiteandadjust_color_balance, run throughImageAdjustmentPixels::runon one thread as Swift's.BlackWhiteSettingsandColorBalanceSettingskeep Swift's fields, defaults and bounds (theisFiniteterms left out, as the bounds refuse NaN and infinities);applythrowsProjectError(invalid)past them and Color Balance hands an untouched image back. Swift'sFilterSettings.normalizeddoes not clamp either, so a number typed past a slider's range fails the preview with that error and a commit setsbrushErrorwith no step, as in Swift: the filter's two workers catchProjectErrorbeside their other errors, where it would otherwise escape asQUnhandledExceptionand abort.FilterKindgains both after Grain (isImageAdjustment),AdjustmentKindInvert, Black & White and Color Balance in Swift's order, withisEditable(all but Invert) andfilterKind;LayerAdjustmentgains the two optionals with Swift's computed getter and setter pairs, validity andapply(Invert throughPixelInvert::run, whole image, colour only); the manifest'sblackWhiteSettingsandcolorBalanceSettingsare Swift's synthesizedCodable, every key required, left out while absent.addAdjustmentopens the editor for editable kinds alone: an Invert layer applies at once. The editors carry both settings throughbeginAdjustmentEditingandeditedAdjustment. 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 alogic_errorfor 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'sisEditabletest). 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).LayerIconsdraws Swift's three new symbols as paths:circle.righthalf.filled,circle.filled.pattern.diagonalline.rectangle(a rounded frame, diagonal lines, a filled circle) andscale.3d(three axes with arrowheads). The filter sheet's two kinds live inUI/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'sif, the rowsm_shownWhenholds 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; eachflagis Swift'sTogglewith its help as the tooltip.NativeLayerListstops listening in its destructor, asContentViewdoes: closing a list window hides it first, and a rename's focus-out then commits into a list half destroyed. Tests: Swift'sinvertAppliesWithoutAnEditorinAdjustmentLayerTests, its editor test over the editable kinds inAdjustmentEditorTestsandFloatingPanelTests,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 inFilterModelTests,LayerAdjustmentTests,AdjustmentPanelTests,CompositorMenusTestsandSelectMenuTests.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/.hare v1.2.3's, with the Camera Raw kernels and Grain's particle size (59b5040); that kernel warns under-Wunused-parameterand-Wmisleading-indentationthrough 1.3.3 (both lines date from9982fad;075a71bfixes another), so CMake silences those two for that file alone.Document/CameraRaw.h/.cpp,CameraRawColor,CameraRawDetailOpticsandCameraRawGeometryCalibrationtwin Swift's four files; Swift'sCameraRaw.swiftruns to 638 lines, so its session extension isCameraRaw+Session.cppwithCameraRaw+Session.h. The enums keep Swift's raw values and order;CameraRawClippingkeeps Swift's bits (highlights 1, shadows 2);BalanceandGainsare structs for Swift's tuples;regionreturns a reference where Swift writes through a key path.CameraRawDragsits inCameraRaw.h, where Swift's sits inCameraRawColor.swift: it holds the whole settings, which C++ needs complete. Swift's panel state sits onFilterEdit(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 inCameraRawPanel,FilterEdit::rawPanel, written throughsetCameraRawPanel, which notifies while an edit is open.renderSettingsis 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.previewJobcarries the panel's views; the preview's worker runsCameraRawScope::preview, whose scope lands on the panel. A fix beyond Swift: Swift'spreviewdrops 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 isQTransform::quadToQuadfrom the image's rect to Swift's corners flipped to y down, drawn smooth and antialiased; a warp Qt cannot solve is alogic_error. Constrain Crop fits the frame insidebrush_alpha_bounds; a crop whose copy finds no memory isExportError(render), where Swift'scroppingreturning nil falls back to the uncropped warp. Left out as dead:apply'sisValidguard and with itisValid(settings arrive normalized, and nothing else asks),nudged's guard for an empty channel (normalizedrepairs every curve to two points;atthrows if that breaks), the vectorscope's wrap of a negative hue (its angle's cosine and sine turn round alike), the parametric curve'smax(0, weight)(a tone inside its region weighs 0 to 1), each group'sapplying(shows:)(no caller), and the defringe eyedropper's ownnormalized(updateFilternormalizes). Equivalent mutants:tone < shadowagainst<=(the regions meet with equal weight), the defringe tie's<against<=, the mixer'sweights > 0against>= 0(a zero weight adds nothing), the identity commit's cancel (the history drops an unchanged step), and Auto'scommittingterm (a committing edit refusesupdateFilter, 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 inImageAdjustmentTests, the kind inFilterModelTests. - The Camera Raw panel (12.8b1, Swift 1.2.3): Swift docks the filter's
NSPanelto 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::showtakes a dock slot,ContentView'spanelDockat the right of the middle row,FloatingPanel::dockedWidth(Swift's 440) wide, and there the panel is its ownPanelWindowmade a plain child (Qt::Widget), soQDialogstill 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 (moveEventasksisWindow), so Gaussian Blur and the rest reopen where they were left, Swift'sdockedPlacementLeavesTheSavedFilterPosition;refocushands a docked panel the keys throughsetFocus, 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:makeKeykeeps its first responder).UI/CameraRawSlidertwins Swift's file:CameraRawSliderTrackwith 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 isSliderSnap'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);displaykeeps a knob under a drag, Swift'sisTrackingValue; the track is fixed at construction: Swift's track update inupdateNSViewhas no twin, since the rows whose tracks change (12.8b2's mixer) are made anew.UI/CameraRawControls.h/.cppandCameraRawControls+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),CameraRawSliderand a 56-pointPickerFieldshowing Swift's.numberto the row's decimals. A row's object name is its settings field (vignetteAmountSlider), since titles repeat. Alt is read fromQGuiApplication::keyboardModifiers, Swift'sNSEvent.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'smouseMoved, 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 arepicking, so the eyedropper shows.EyedropperIcon::iconcame fromLevelsSheet, which shares it. Left out as dead: the canvas'skind == cameraRawand edit terms before the readout (the session asks), a hide of the other placement's panel inshow(every edit closes its panel first), Swift's clipping reset before a row's reset (assignclears 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.cppand+Grading.cpp,UI/CameraRawDetailOpticsControlsandUI/CameraRawGeometryCalibrationControlstwin Swift's three files, each view aQWidgetover the session, synchronized onchanged().UI/CameraRawRowis Linux-only: the row each Swift view builds for itself (sharpenSlider,opticsSlider,geometrySlider,calibrationSlider, the curve's and mixer's sliders, 12.8b1'sslider): 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 arecameraRawSegments, touching checkable buttons in an exclusiveQButtonGroup. 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,repairdrops the second as a duplicate and the double tap removes one. The graph isCameraRawCurveGraph: a class defined in a.cppoutside an anonymous namespace is global, and the Curves sheet'sCurveGraphwould 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 aQConicalGradient, which runs counterclockwise from the right as the hue does (1.2.9's96edd5f), takes presses and double clicks inside its circle alone (Swift'scontentShape), and Swift'stheDockedPanelFitsTheGradingWheelsholds; 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,scopeandline.diagonal, areLayerIconspaths. 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 (normalizedclamps 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.hwalked byCameraRawGroupRowsTests(every row against Swift's values),CameraRawGroupDrawTests(drawing, pages, gestures, the panel throughFilterSheet),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 inCompositorMenusTests. - Hash containers on the render path (12.8e): Qt 6.4 declares
QHashPrivate::Data::findOrInsertnoexceptwhile it allocates, so aQHashorQSetinsert that finds no memory callsstd::terminate, past every catch (found by a backtrace ofProjectExportFailureTestsunder load: the JPEG worker's render aborted inLiveMaskGraph::validate). Code a worker runs under a catch that promises to survive memory running out usesstd::mapandstd::set, whose inserts throw:LiveMaskGraph::validate,FolderMaskClip::drawandLiveMaskRenderer's caches (andLiveMaskBaker::bakebesidevalidate, though its workers catchExportErroralone, so abad_allocthere still ends the app, by design), asImageExporter::renderalready keyed its records. The UI thread's ownQHashes stay: a failure there ends in Qt's event loop either way.RenderStarvationTestsreplacesoperator newon its thread and fails each allocation ofvalidate,prepareStacksandFolderMaskClip::drawin turn: each must throwstd::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).AdjustmentKindgains Add Noise, Gaussian Blur and Motion Blur after Grain, v1.2.3's order (it moved Add Noise ahead of the blurs), withcircle.dotted,drop.fillandwindasLayerIconspaths.LayerAdjustmentkeeps 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, andsamplingMargin; the manifest writes each only when set, the seed aUInt32read as Grain's; the store takes the three kinds from version 9, which it now writes (versions 1–9).applyruns the three throughPixelFilter::runwith the layer's own seed;FilterJob::noiseOriginsits last, so the positional initialisers keep their order, andkernels/NoisePixels.cis 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 andscaleare already the surface's pixels; the twin's surfaces hold device pixels, soLiveMaskRenderer::adjustpassesadjustmentScale(the canvas's zoom, Swift's, 1 elsewhere) times the device's pixels a region unit, andapplycounts 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::drawtakes Swift'spaddingin 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 visiblesamplingMargintimes the zoom, and its crisp path (from 200%) now draws the layers through a padded surface as well, as Swift'sdrawLayerswraps 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'sdrawLayerswraps 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/ImageTrimtwins Swift's file:TrimBasedOnby 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) andtrim(the export's render, thenCanvasResizerby the content offset, or the snapshot itself when nothing is cut).UI/TrimSheetis 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 throughNativeShortcut.ProjectController::trimruns as Canvas Size does, throughresizeProject, 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),trimImageand the session'strim(called by Swift's tests alone; the twin tests the rect and the controller's two steps), the kept rect'smaxX > minXguard (the early returns leave content, so left < right and top < bottom) and the colour scan'slast > first(the row's first unmatched pixel stops it). A fix beyond Swift: a raster that cannot be converted isExportError(render), explained, where Swift'stry?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::innerGlowlast;LayerEffectsKernelblurs 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'sinnerGlowRendersInsideSourceWithoutBoundsExpansion), the stroke surface's reach three sizes and two. Swift's CPUinnerGlowCoverageis 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'sopacity > 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 inLayerAdjustmentTests,AdjustmentEditorTests,AdjustmentLayerTestsandAdjustmentPanelTests, the glow inLayerEffectsKernelTests' replay,LayerEffectsSurfaceTests,EffectsPreviewCacheTests,LayerEffectsTestsandEffectsPanelTests, version 9 across the store's tests, Trim inCompositorMenusTests. - Drops between projects:
ProjectWorkspace::draggedLayerSourcefinds the tab whose document holds the dragged id (Swift reads the drag pasteboard; the twin reads the drag'sQMimeData, which every drop target has),canReceiveDrag(data, destination)refuses only a layer's own tab, andreceiveProviderscopies several ids one after another, as Swift awaits each provider, so a two-row drop on the New button opens two tabs as Swift's does.ContentViewtakes such a drop over its whole area (setWorkspacehands it the workspace;acceptsDropkeeps Swift's guards: busy, the sheet, the importer, a rename;levelsarrives with 9.x), shows the 3-point accent ring inside the canvas while targeted and hands the drop's document point on when it fell on the canvas.ProjectTabButtontakes drops for its tab (accent fill and rim, "Add to