Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
PhosphorEngine::WindowPlacementStore Class Reference

The single source of truth for window restore state in the unified model. More...

#include <phosphor-engine/include/PhosphorEngine/WindowPlacementStore.h>

Public Member Functions

 WindowPlacementStore ()=default
 
bool record (WindowPlacement placement)
 Record / MERGE this window's placement.
 
std::optional< WindowPlacementtake (const QString &windowId, const QString &appId, const std::function< bool(const WindowPlacement &)> &accept={}, const std::function< bool(const WindowPlacement &)> &preferred={})
 Restore lookup: the first record whose accept predicate passes, trying the same-instance match before the appId FIFO (oldest first).
 
std::optional< WindowPlacementtakeForReopen (const QString &engineId, const QString &windowId, const QString &appId, const QString &screenId)
 Reopen resolve: the shared consumption pattern the TILING engines' open-time restores use (SnapEngine::resolveWindowRestore keeps its own take + re-bind: its snapped records restore cross-screen and its accept depends on a mode-defer bypass, so rule 1 below does not fit it — and note the snap path therefore also keeps take()'s oldest-first order WITHOUT the live-instance exclusion below; that asymmetry is a documented property of the snap flow, not an oversight) — take() wrapped in the accept predicate both tiling engines share and the two rules that make a close/reopen (fresh uuid, appId-FIFO match) behave correctly.
 
std::optional< WindowPlacementpeek (const QString &windowId, const QString &appId, const std::function< bool(const WindowPlacement &)> &accept={}) const
 Non-consuming lookup (unlike take): the record for the same live instance, else the NEWEST record in the appId bucket whose accept passes.
 
std::optional< WindowPlacementpeekExact (const QString &windowId) const
 Same-instance peek: branch 1 of peek() only, never the appId-FIFO fallback.
 
std::optional< WindowPlacementpeekForReclaim (const QString &windowId, const QString &appId, const std::function< bool(const WindowPlacement &)> &accept={}) const
 Non-consuming lookup for the cross-screen reclaim (IPlacementEngine::claimCrossScreenReopen).
 
bool contains (const QString &windowId, const QString &appId=QString()) const
 True if a record exists for the same live instance, or (if appId non-empty) any record in that appId bucket.
 
void setLiveInstanceProbe (std::function< bool(const QString &windowId)> probe)
 Inject the live-window probe takeForReopen's appId fallback uses to skip records bound to a still-open window (see its doc).
 
bool collapsePureFloatSiblings (const QString &appId, const QString &keepWindowId)
 Collapse stale pure-float duplicates for an app, keeping keepWindowId.
 
bool clear (const QString &windowId)
 Drop any record for the same live instance (and prune the empty bucket).
 
bool clearFreeGeometry (const QString &windowId)
 Clear ONLY the shared free/float geometry for the same live instance, leaving the engine slots and context intact.
 
bool clearFreeGeometry (const QString &windowId, const QString &screenId)
 
bool releaseEngineSlot (const QString &windowId, const QString &engineId)
 DOWNGRADE one engine's slot to WindowPlacement::stateReleased() on every record for the same live instance, leaving the other engines' slots, the context and the shared geometry intact.
 
int transform (const std::function< bool(WindowPlacement &)> &fn)
 Apply an in-place mutation to every record; fn returns true when it changed the record.
 
int removeIf (const std::function< bool(const WindowPlacement &)> &pred)
 Remove every record matching pred. Returns the count removed.
 
QList< WindowPlacementrecords () const
 All records, in no particular order.
 
QJsonObject serialize (const std::function< bool(const WindowPlacement &)> &keep={}) const
 JSON shape: { appId: [ record, ... ] }.
 
void deserialize (const QJsonObject &obj)
 
int size () const
 

Static Public Attributes

static constexpr int MaxPerApp = 16
 Per-app record cap (public so tests can pin the eviction contract).
 

Detailed Description

The single source of truth for window restore state in the unified model.

Holds AT MOST ONE WindowPlacement record PER WINDOW INSTANCE (not per engine), captured live by the engines. Records are keyed two ways so they survive both a daemon restart (the instance component is stable while the window stays open) and a close→reopen (uuid changes, so the appId FIFO carries it).

The core invariant — record() MERGES into the single record for the same instance — gives per-mode state independence with a shared free/float geometry: each engine updates only its OWN slot (in engines, keyed by engineId()) plus any free-geometry change, so a window may be snapped in the snap engine AND floating in the autotile engine at once, each engine remembering the window's state in its own mode, while the un-managed position lives once in freeGeometryByScreen (shared across modes, keyed per screen).

Constructor & Destructor Documentation

◆ WindowPlacementStore()

PhosphorEngine::WindowPlacementStore::WindowPlacementStore ( )
default

Member Function Documentation

◆ clear()

bool PhosphorEngine::WindowPlacementStore::clear ( const QString &  windowId)

Drop any record for the same live instance (and prune the empty bucket).

Returns true if a record was actually removed.

◆ clearFreeGeometry() [1/2]

bool PhosphorEngine::WindowPlacementStore::clearFreeGeometry ( const QString &  windowId)

Clear ONLY the shared free/float geometry for the same live instance, leaving the engine slots and context intact.

Returns true if anything was cleared. The all-screens form is for wholesale invalidation (virtual-screen remap); the screen-scoped overload is for consume-once paths (drag-out, drop-snap), which must not destroy the float-back remembered for other monitors.

◆ clearFreeGeometry() [2/2]

bool PhosphorEngine::WindowPlacementStore::clearFreeGeometry ( const QString &  windowId,
const QString &  screenId 
)

◆ collapsePureFloatSiblings()

bool PhosphorEngine::WindowPlacementStore::collapsePureFloatSiblings ( const QString &  appId,
const QString &  keepWindowId 
)

Collapse stale pure-float duplicates for an app, keeping keepWindowId.

A "pure-float" record carries float-back geometry but NO managed (snapped/tiled) engine slot. When keepWindowId names a pure-float record, every OTHER pure-float record in the same appId bucket that remembers a float position on a screen keepWindowId also covers is removed. Records carrying a snapped/tiled slot are never touched (managed placements whose multi-instance distribution must survive). No-op when the kept record is absent or itself managed.

Called ONLY from close-capture paths: a window closing floating is the freshest authority for its app's float-back on that screen, so duplicate siblings (left by rapid open/close or overlapping short-lived instances) are stale. Records bound to a still-OPEN window (per the live-instance probe) are never pruned, and a pruned sibling's engine slots and other-screen geometry are absorbed fill-gaps-only. Without the collapse, a consuming reopen — take()'s oldest-first for snap, or a probe-excluded tail for the tiling engines — can rotate a reopening window between the duplicates: it "opens in a different spot each time."

Returns true if at least one sibling was removed, so the caller can mark its persistence dirty: the preceding record() may have been a content-identical no-op, in which case this prune is the only mutation and would otherwise not reach disk until an incidental save.

◆ contains()

bool PhosphorEngine::WindowPlacementStore::contains ( const QString &  windowId,
const QString &  appId = QString() 
) const

True if a record exists for the same live instance, or (if appId non-empty) any record in that appId bucket.

◆ deserialize()

void PhosphorEngine::WindowPlacementStore::deserialize ( const QJsonObject &  obj)

◆ peek()

std::optional< WindowPlacement > PhosphorEngine::WindowPlacementStore::peek ( const QString &  windowId,
const QString &  appId,
const std::function< bool(const WindowPlacement &)> &  accept = {} 
) const

Non-consuming lookup (unlike take): the record for the same live instance, else the NEWEST record in the appId bucket whose accept passes.

Leaves the store unchanged — for live reads such as the float-back geometry lookup, where the record must stay put for the eventual restore/capture.

◆ peekExact()

std::optional< WindowPlacement > PhosphorEngine::WindowPlacementStore::peekExact ( const QString &  windowId) const
inline

Same-instance peek: branch 1 of peek() only, never the appId-FIFO fallback.

The instance component remains stable if a live window's appId prefix changes, while still distinguishing same-app siblings. The appId fallback exists for close/reopen paths where the instance changes.

◆ peekForReclaim()

std::optional< WindowPlacement > PhosphorEngine::WindowPlacementStore::peekForReclaim ( const QString &  windowId,
const QString &  appId,
const std::function< bool(const WindowPlacement &)> &  accept = {} 
) const

Non-consuming lookup for the cross-screen reclaim (IPlacementEngine::claimCrossScreenReopen).

Differs from peek() in the two ways that make a reclaim verdict sound:

  • It applies the LIVE-INSTANCE exclusion takeForReopen's appId fallback applies: a record bound to a still-OPEN sibling describes a different, living window, never this window's history, so it must not justify a cross-screen pull. peek() deliberately lacks the exclusion (float-back geometry reads legitimately consult a live sibling's record); a reclaim through plain peek() teleported a fresh second instance onto its open sibling's monitor on EVERY open, mid-session, repeatedly.
  • It scans ONLY the appId bucket (the window's own record included — a same-instance match wins outright, live or not, since the window's own record IS its history). peek()'s same-instance branch walks every bucket in the store; on the per-open reclaim path that cost is paid per engine per open. The narrowing is safe because record() RE-BUCKETS on an appId change, so a record follows the window's current appId — if that ever stops holding, this scan starts missing records peek() would find. It fails SAFE either way: a miss means no reclaim, never a wrong one. Returns nullopt when appId is empty — a bare id has no bucket, so a reclaim verdict is impossible.

◆ record()

bool PhosphorEngine::WindowPlacementStore::record ( WindowPlacement  placement)

Record / MERGE this window's placement.

The incoming record supplies only the calling engine's slot (in engines) and any free-geometry update; if a record for the same live instance already exists (in any appId bucket) the incoming engine slot(s) and free-geometry screen(s) are merged in, leaving the other engine's slot and other screens' free geometry intact. Otherwise the record is appended to its appId's FIFO. No-op on an invalid record. Returns true if the store actually changed — false when the merge produced a content-identical record, so callers can skip marking state dirty and avoid a self-perpetuating save loop. Stamps a fresh monotonic sequence whenever it changes anything (the content-identical short-circuit leaves the existing record, sequence included, untouched).

◆ records()

QList< WindowPlacement > PhosphorEngine::WindowPlacementStore::records ( ) const

All records, in no particular order.

For read-only sweeps (e.g. building the effect's instant-restore cache from the snapped records).

◆ releaseEngineSlot()

bool PhosphorEngine::WindowPlacementStore::releaseEngineSlot ( const QString &  windowId,
const QString &  engineId 
)

DOWNGRADE one engine's slot to WindowPlacement::stateReleased() on every record for the same live instance, leaving the other engines' slots, the context and the shared geometry intact.

Returns true if any record changed.

This is the release-path counterpart of record()'s merge-never-clear: slots accumulate as cross-mode memory, and for RESTORE that is right — but a slot on a window an engine has knowingly GIVEN UP (cross-mode handoff) is not memory, it is a stale ownership claim. Since the cross-screen reclaim (pendingCrossScreenManagedRestore) treats a managed slot plus the record-level screenId as evidence of a home to pull the window back to, a stale slot that outlives its engine's ownership can yank a window out from under its CURRENT engine whenever the new owner's capture misses (the record()-overwrite of screenId is not guaranteed on every path).

DOWNGRADE, not remove, and the difference is load-bearing on both sides — see stateReleased()'s contract. Removing the slot would also risk emptying the engines map, which record()'s merge reads as "no managed context to adopt", freezing the record's screenId against every later geometry-only write.

Sweeps ALL records matching the instance rather than stopping at the first: appId drift (the Electron/CEF case this codebase canonicalizes for) can leave records for one instance in two buckets, and the one carrying the stale slot is not necessarily the one QHash order reaches first.

Callers: the tiling engines' handoffRelease, through WindowTrackingService::releaseEngineSlot (which marks the store dirty). NOT called on ordinary close — a window that CLOSED tiled keeps its slot; that persistence is exactly what login restore reads.

◆ removeIf()

int PhosphorEngine::WindowPlacementStore::removeIf ( const std::function< bool(const WindowPlacement &)> &  pred)

Remove every record matching pred. Returns the count removed.

◆ serialize()

QJsonObject PhosphorEngine::WindowPlacementStore::serialize ( const std::function< bool(const WindowPlacement &)> &  keep = {}) const

JSON shape: { appId: [ record, ... ] }.

keep filters out entries that should not persist (e.g. disabled-context). Empty buckets are dropped.

◆ setLiveInstanceProbe()

void PhosphorEngine::WindowPlacementStore::setLiveInstanceProbe ( std::function< bool(const QString &windowId)>  probe)
inline

Inject the live-window probe takeForReopen's appId fallback uses to skip records bound to a still-open window (see its doc).

Answers per full windowId; evaluated at consume time. Unwired (tests) means no exclusion.

◆ size()

int PhosphorEngine::WindowPlacementStore::size ( ) const

◆ take()

std::optional< WindowPlacement > PhosphorEngine::WindowPlacementStore::take ( const QString &  windowId,
const QString &  appId,
const std::function< bool(const WindowPlacement &)> &  accept = {},
const std::function< bool(const WindowPlacement &)> &  preferred = {} 
)

Restore lookup: the first record whose accept predicate passes, trying the same-instance match before the appId FIFO (oldest first).

The matched record is REMOVED (consumed) and returned. accept lets the caller reject cross-screen / disabled-context / wrong-kind candidates.

preferred (optional) ranks the appId-FIFO branch ONLY: when supplied, the oldest entry satisfying BOTH accept and preferred is consumed first, and only if none qualifies does the oldest merely-accepted entry win. The same-instance match is unaffected — a window's own record is always used in whatever state it holds. Lets a caller restore the most meaningful record (e.g. a snapped placement) ahead of a contentless free/floating sibling that is merely older in the FIFO.

◆ takeForReopen()

std::optional< WindowPlacement > PhosphorEngine::WindowPlacementStore::takeForReopen ( const QString &  engineId,
const QString &  windowId,
const QString &  appId,
const QString &  screenId 
)

Reopen resolve: the shared consumption pattern the TILING engines' open-time restores use (SnapEngine::resolveWindowRestore keeps its own take + re-bind: its snapped records restore cross-screen and its accept depends on a mode-defer bypass, so rule 1 below does not fit it — and note the snap path therefore also keeps take()'s oldest-first order WITHOUT the live-instance exclusion below; that asymmetry is a documented property of the snap flow, not an oversight) — take() wrapped in the accept predicate both tiling engines share and the two rules that make a close/reopen (fresh uuid, appId-FIFO match) behave correctly.

The accept predicate, hoisted here so the two engines cannot drift: a record whose engineId slot is FLOATING restores when its screen matches screenId (or is empty), and FIFO consumption by a DIFFERENT instance additionally requires a valid anyFreeGeometry (a geometry-less floating record is meaningful only same-instance — consumed by a sibling it floats a fresh window at its spawn rect for no reason while burning a FIFO slot). FLOATING slots only, deliberately: a TILED record is never consumed and restores no position — its role is the exact-final verdict below (the window closed tiled, so the reopen must not float it).

  1. A REJECTED exact record is FINAL — no FIFO fallback past it — but ONLY when that record carries a slot for the ASKING engine. The fallback exists for a reopen, whose fresh uuid has no exact record WITH A VERDICT: every open writes a geometry-only, slot-less record under the live uuid (the pre-tile free-geometry capture) before the engine's restore runs, and that stub says nothing about this engine, so it must not veto the FIFO. A LIVE window whose own record holds this engine's slot but was rejected on context (tiled on another desktop, say) IS final: falling through would consume a SIBLING's record, and the re-bind below would re-record it under this window's id, where the merge overwrites the window's own other-context slot.
  2. The consumed record is RE-BOUND to the live windowId and re-recorded, so the other engines' slots + per-screen free/float geometry survive the reopen.

The appId fallback consumes the NEWEST accepted record — matching peek()'s "the most recent placement is current truth" — and never one whose window instance is still LIVE (per the live-instance probe): the last close is the state the user expects back, and consuming oldest first handed a reopen whichever stale record had sat unconsumed longest (the octopi graveyard: eleven leftover tiled records shadowing the fresh floating one, and colliding months-old column ranks on a compositor-restart restore). The live exclusion is what makes newest-first safe for multi-instance apps: a record just re-bound to an OPEN sibling is the newest in the bucket, and without the probe the next reopen would steal it, leaving the sibling recordless.

Returns the consumed record (already re-recorded), or nullopt when no record passed.

◆ transform()

int PhosphorEngine::WindowPlacementStore::transform ( const std::function< bool(WindowPlacement &)> &  fn)

Apply an in-place mutation to every record; fn returns true when it changed the record.

Returns the number changed. For bulk rewrites that keep the appId bucketing (e.g. virtual-screen id remap of freeGeometryByScreen keys). Does NOT move records between buckets — only mutate fields other than appId.

Member Data Documentation

◆ MaxPerApp

constexpr int PhosphorEngine::WindowPlacementStore::MaxPerApp = 16
staticconstexpr

Per-app record cap (public so tests can pin the eviction contract).


The documentation for this class was generated from the following file: