diff --git a/LICENSE-GPL b/LICENSE-GPL index cb82534a8de43d..74cac2d7714d09 100644 --- a/LICENSE-GPL +++ b/LICENSE-GPL @@ -1,6 +1,8 @@ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 +UNSTAGED CHANGES + Copyright © 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. diff --git a/README.md b/README.md index 84f536569a32ce..75fa055e60db86 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of --- +STAGED CHANGES + ### Installation On macOS, Linux, and Windows you can [download Zed directly](https://zed.dev/download) or install Zed via your local package manager ([macOS](https://zed.dev/docs/installation#macos)/[Linux](https://zed.dev/docs/linux#installing-via-a-package-manager)/[Windows](https://zed.dev/docs/windows#package-managers)). diff --git a/crates/gpui/docs/read_tracked_invalidation.md b/crates/gpui/docs/read_tracked_invalidation.md new file mode 100644 index 00000000000000..57ebf79efecf0a --- /dev/null +++ b/crates/gpui/docs/read_tracked_invalidation.md @@ -0,0 +1,493 @@ +# Read-Tracked Invalidation: A Rendering & State Redesign for GPUI + +**Status:** Design proposal. Milestone 1 is specified below (§9) as a +self-contained work order. Nothing in this document has shipped. + +**One-line summary:** Stop deciding *what to re-render* with entity identity and +notify routing; decide it with the read ledger GPUI already maintains — extended +from window granularity down to element-tree boundaries — and reserve `notify()` +exclusively for the data plane. + +**Terms used throughout** (skim now, refer back later): + +| Term | Meaning | +|---|---| +| *entity* | A state cell of type `T` behind an `Entity` handle, living in the app-wide `EntityMap`. Persistent, refcounted, identified by `EntityId`. | +| *notify / observe* | The explicit event system of the entity graph. `cx.notify()` announces "my data changed"; `cx.observe(&entity, ...)` registers a callback that runs when it does. Callbacks run from an effect queue (`App::flush_effects`, `crates/gpui/src/app.rs` ~1610). | +| *element* | A node in the per-frame UI tree built by the fluent DSL (`div().child(...)`). Elements are rebuilt every frame; they are values, not objects. | +| *element state* | Per-position storage that survives across frames, keyed by `GlobalElementId` (the stack of element ids from root to here). Lives in the frame (`Frame::element_states`). | +| *`use_state` / `use_keyed_state`* | Hooks (`crates/gpui/src/window.rs` ~3648) that allocate an `Entity` whose lifetime is tied to an element position: "exists as long as this element renders in consecutive frames." | +| *struct `View` trait* | `gpui::View` (`crates/gpui/src/view.rs` ~182): lets a plain struct act as a view, reporting an optional `EntityId` as its identity. Distinct from the classic entity-backed view, `Entity`. | +| *the forwarder* | An observer installed by `use_keyed_state`: when the allocated state entity notifies, call `cx.notify(current_view)` — where `current_view` is the identity of the nearest enclosing view (`window.rs` ~3661). This document argues for its deletion. | +| *boundary* | An element-tree node that can independently skip re-rendering by reusing its previous frame. Today the only boundaries are cached entity-backed views (`Entity::cached`); this design generalizes them. | +| *ledger / read-set* | The record of which entities (and, later, globals) were read. GPUI already records every entity read (`EntityMap::accessed_entities`, `crates/gpui/src/app/entity_map.rs` ~156-179). A boundary's read-set is the slice of the ledger attributed to it. | +| *amplification* | The pattern this design eliminates: a component observing data it doesn't own must `cx.notify()` itself unconditionally, because self-notification is the only way to bust its identity-keyed cache. | +| *Projection* | From an unmerged branch: a lens handle (`Projection

` / `ProjectionMut

`, `crates/gpui/src/projection.rs`) onto part of an entity's state — e.g. a `ProjectionMut` addressing one field of a form entity. Reviewing that branch is what surfaced this redesign (§2). | + +--- + +## 1. The three registers of GPUI + +GPUI programs are written in three registers, each with its own shape, lifetime, +and control flow — and each valuable precisely because of what it gives up: + +| Register | Shape | Time | Control flow | Written as | +|---|---|---|---|---| +| **Entity graph** | arbitrary graph | persistent across frames | push: explicit events | plain Rust structs behind `Entity` handles | +| **Element tree** | tree | one frame, memoized | pull: demand-driven | the fluent DSL; components | +| **Imperative drawing** | straight line | one instant | sequential command emission | `Element` impls: layout, prepaint, paint | + +The entity graph can take any shape a domain needs — `Project`'s object graph +has nothing to do with rendering, and shouldn't. Its identity notion is +**allocation**: an `EntityId` minted at creation, lifetime governed by +refcounts. Its ethos is **explicitness**: no auto-notification anywhere; an +entity changes when its code says it changed. The drawing register at the other +end has no identity at all — just order — which is exactly where its +immediate-mode speed comes from. + +The element tree mediates. Rendering is a pipeline of shape-collapses: the tree +is a per-frame projection of the graph, and the command stream is a per-frame +projection of the tree. There is deliberately no graph↔commands edge in either +direction; the tree is the only adapter between the shape regimes. + +The tree register was accreted rather than designed. Interaction state forced +its existence: a click is stateful — a press and a release that must find each +other across frames — and its identity is **positional** ("the third button in +the toolbar"), a place that may correspond to no domain object whatsoever. GPUI +v1 forced graph identity onto that state — mint an entity for every hoverable +widget — and drowned in bookkeeping. So the tree grew its own organs, one +pragmatic patch at a time: element ids to name positions, element state to store +things at them, `use_state` to allocate entities with positional lifetimes, the +struct `View` trait and `current_view` to route invalidation. In the tree, +identity = position (`GlobalElementId` path) and lifetime = continuity of +rendering. These are different notions of "same thing across time" than the +graph's, and they are not interconvertible. + +By now the tree has almost every organ of a complete register: identity +(`ElementId`), storage (element state), composition (the DSL, components), even +memoization scaffolding (prepaint reuse). The one organ it never grew is **a +dependency semantics of its own** — an answer to "when is this position stale?" +It borrowed the graph's answer (notify), which is addressed by `EntityId`, so +the tree had to route its invalidation *through graph identity*. That borrowed +organ is `current_view`, and every bug documented in §2 is a register-confusion +bug traceable to it. This design gives the tree its own answer — stale when +something it **read** changed, or when explicitly invalidated in the tree's own +currency — and settles the tree's relationship to the graph into two typed +edges: reads down, writes up. + +One rehabilitation up front: `use_state` is not the problem and survives +unchanged. Tree-lifetime allocation of graph-register state is the correct +bridge — an editor needs observation, async, real entity-hood, but its lifetime +is genuinely positional. Only the forwarder stapled to it is wrong. The +practical rule for app authors becomes: choose the register by asking what a +piece of state's identity *is*. A concept in your domain → entity. A place in +the UI → element state. A domain concept living at a place → `use_state`. + +--- + +## 2. How we got here + +This design fell out of a code review of the projections branch, which surfaced +a chain of increasingly fundamental problems: + +1. **A hang.** A component identified by a data entity's id (via the struct + `View` trait), containing `use_state`-allocated state that observes that same + entity, loops forever inside `App::flush_effects`. Reproduced on `main` with + plain entities — no projections involved — using the `view_example` `Input`: + on the first write to the value, the effect queue never drains. This is not a + projections bug. + +2. **The loop's anatomy.** Three locally-innocent pieces in three files: + - *Data → derived state* (component author): the editor observes its value + and notifies itself when its cursor clamps + (`crates/gpui/examples/view_example/example_editor.rs`). + - *Internal state → view identity* (framework): the forwarder — + `cx.observe(&state, move |_, cx| cx.notify(current_view))` + (`window.rs` ~3661), `current_view` being the nearest `View::entity_id` + (`view.rs` ~323, `window.rs` ~4670). + - *The alias* (component): `View::entity_id` returning the **data entity's** + id, merging "the widget" and "the widget's input data" into one graph node. + The forwarder then publishes widget-internal churn as data-change events of + an entity upstream of the widget's own state. Cycle. See §10 for the minimal + reproduction. + +3. **The three roles of an `EntityId`.** The diagnosis underneath the loop — + referenced throughout this document: + - **Role 1 — element identity:** keying element state, caching, sibling + disambiguation. + - **Role 2 — invalidation target:** "redraw the subtree rendering this." + - **Role 3 — publication channel:** "this entity's data changed; run its + observers." + + Roles 1 and 2 are safe to borrow from another entity; role 3 is not. The + struct `View` trait hands out all three as a bundle, and its doc comment + coaches the unsafe borrow ("a view typically holds the backing entity as a + field and returns its `EntityId` here"). + +4. **Amplification.** The identity rule doesn't just permit loops; it causes + over-rendering. The only cache-bust signal today is "identity entity + notified" (`view.rs` ~390), so any component rendering data it doesn't own + must convert upstream maybe-changes into unconditional self-notifies. The + editor's cursor-clamp handler cannot be written in settling style (notify + only when something actually changed) without breaking `editor.cached(..)`. + At app scale this is the Pane-observes-items-and-notifies-itself pattern: + whole-region re-renders purchased to bust one cache. + +5. **The realization.** GPUI is *already* a read-tracked reactive system at + window granularity. Every entity read lands in the ledger + (`entity_map.rs` ~156-179). `Window::draw` harvests it + (`record_entities_accessed`, `app.rs` ~1095), and `App::notify` + (`app.rs` ~2611) dirties exactly the windows that read the notified entity. + **Reads already decide *where* to re-render at the top level.** The element + tree below just never got the same rule — it got the identity rule instead. + Cached views even record per-subtree read-sets already + (`ViewElementState.accessed_entities`, captured via + `detect_accessed_entities`, `app.rs` ~1079 / `view.rs` ~405) and then consult + them for nothing: `view.rs` ~390 checks only identity, and `view.rs` ~395 + merely re-registers the reads on reuse. + +Before committing to the design, we commissioned an adversarial review: an agent +with full code access, instructed to build the strongest possible case against +the proposal and to rank objections by severity. Its findings materially +reshaped the design (the boundary-invalidation channel of §5 and the `Cached` +component of §6 both exist because of it) and are recorded with dispositions in +§7. + +--- + +## 3. The model + +Two planes, tied by exactly three links, each with one-way arrows: + +| Link | Direction | Nature | +|---|---|---| +| **Reads** | entity graph → element tree | Implicit. Recorded in the ledger; drives all render invalidation. | +| **Allocation** (`use_state`) | element tree → entity graph | Explicit. Ownership, not an event edge. | +| **Event handlers** | element tree → entity graph | Explicit user code; the only writes. | + +Rules: + +- **Entity plane (unchanged — Zed's ethos):** `cx.notify()` is explicit and + means "my data changed." Observers are the explicit data plane (role 3). + No auto-notifies. +- **Render plane (new rule):** a boundary re-renders iff its recorded read-set + intersects the set of entities notified (or globals updated) since it was last + rendered — or it was explicitly invalidated through the render-plane channel + (§5). Renders are memoized pulls. Elements never bust other elements; render + output is not an input to anything, so invalidation cannot cascade through the + tree. +- **For honest entity-backed views nothing changes:** an `Entity` + view reads itself during render, so the read rule strictly subsumes the + identity rule for it. The rules diverge only for borrowed identities — where + the read rule works and the identity rule loops. + +The forwarder is deleted. Internal state entities are dependencies of the +boundaries that read them; their explicit notifies reach those boundaries +through the ledger like any other entity's. + +--- + +## 4. Bookkeeping: attribution by ownership, not draw order + +The naive implementation — one append-only ledger, each boundary owning a +contiguous `[start..end)` range — is broken by GPUI's own draw structure: +paint-phase reads happen outside the prepaint capture window (`view.rs` +~405-415 wraps prepaint only), and deferred draws (`window.rs`, +`prepaint_deferred_draws`) execute after the main walk, re-rooted, so their +reads land outside their logical parent's range. A global index-*set* +additionally mis-attributes sibling reads (the second reader's insert is a +no-op). + +All three problems share one wrong assumption: that attribution follows +**wall-clock draw order**. It should follow **logical ownership**: + +- Maintain a **stack of open dependency records**. Opening a boundary pushes its + record; every read (`EntityMap::read`/`read_any`, global access) appends the + `DependencyId` to the innermost open record. +- **Phases are episodes.** A boundary's record is re-opened during its paint + walk; paint reads append to the same record. Busting always evaluates *last + frame's completed record*, so capture time never races the reuse decision. + (Reading during paint remains bad form — that's what layout and prepaint are + for — but it is captured, not silently dropped. A debug lint can come later.) +- **Deferred draws re-open their originator's record.** A `DeferredDraw` already + carries its originating view; it carries its originating *boundary record* + instead. When the deferred element (a context menu, tooltip, drag overlay) + prepaints in a later round, push that record — its reads attribute to the + boundary that deferred it, wherever the element lands in draw order. No + discontiguous-range arithmetic; multi-phase rendering stops being a special + case because every phase re-establishes which boundary is logically open. +- **Dedup in O(1):** keep a per-frame `last_attributed: FxHashMap`; on read, if `last_attributed[dep]` equals the current innermost + record, skip the append. This replaces (and costs about the same as) today's + global hash-set insert. +- **Parents don't need children's reads.** Busting uses a per-window reverse + index `DependencyId → SmallVec` maintained at record time. A dirty + boundary marks its **ancestor path** dirty so the top-down draw can reach it — + and `GlobalElementId` is the stack of element ids, so ancestry is a prefix + relation (this replaces `mark_view_dirty`'s dispatch-tree walk, `window.rs` + ~1936). Ancestors re-run their render functions; clean boundaries off the + dirty paths splice their previous prepaint ranges, exactly as `reuse_prepaint` + does today. +- **Fan-out tier for widely-read dependencies.** Theme- and settings-class + dependencies are read by effectively every boundary. When a dependency's + reader count exceeds a threshold, flip it to a *broad* flag: notifying it + refreshes the window (which is today's behavior for those cases anyway) + instead of enumerating readers. This bounds reverse-index size and answers the + bust-storm objection (§7, objection 5). +- **Lifecycle:** records and reverse-index entries are generation-stamped and + expire with the element state they belong to. On cache reuse, the stored + record re-registers into the window aggregate, as `extend_accessed` does now + (`view.rs` ~395). +- **Globals** join the ledger as a second variant; `update_global` dirties + readers through the same index. Window state (focus, hover position, mouse) + stays on today's coarse whole-window refresh initially and can be promoted + later. + +```rust +enum DependencyId { + Entity(EntityId), + Global(TypeId), +} +``` + +--- + +## 5. The render-plane invalidation channel + +The adversarial review's deepest finding: `cx.notify(current_view)` is +load-bearing far beyond the forwarder. Hover styling, active/click state, +scroll, tooltips, `request_animation_frame`, image loads, list state — the +interaction layer (call sites reported across `div.rs`, `text.rs`, `list.rs`, +`img.rs`, `window.rs`; verify the full list during Milestone 2) all say +"re-render me" by notifying the nearest view entity. Every one of those is +role 2 (invalidation) wearing role 3's (publication) clothes: hover does not +want observers to run; it wants its region repainted. This is the same +role-laundering as the forwarder, in the opposite direction — and it is why +"just delete view identity" would leave a vacuum. + +So the channel these call sites actually need becomes first-class: + +```rust +window.invalidate(boundary_id) // dirty this boundary for the next frame. + // No Effect::Notify. No observers. Not an entity. +``` + +Interaction and animation state dirties the boundary it occurred within, +addressed by element id — the tree's own identity currency. Explicit +boundary-dirty overrides cache reuse regardless of read-sets and props. The +draw-phase suppression carve-out (`window.rs` ~157-164: notifications during a +draw mark state dirty but defer effects) carries over unchanged. Entities stop +hearing about hover; observers of an editor fire only when the editor *says* +something changed. + +This resolves the review's two hardest objections at once: interactive content +inside cached subtrees stays live (the thing that made universal caching +unsound), and removing struct-view `entity_id` leaves no vacuum, because the +thing that identity was actually being used for gets its own addressing scheme. + +--- + +## 6. The `Cached` component + +Caching stops being ambient (`.cached()` sprinkled on arbitrary elements — which +the review showed to be unsound) and becomes an explicit, visible boundary +component: + +```rust +Cached::new( + key, // ElementId: state slot + sibling disambiguation (role 1, tree currency) + props, // T — the data funnel into the subtree + render, // fn(&T, &mut Window, &mut App) -> AnyElement ← plain fn, NOT a closure +) +.compare(|prev, next| ...) // user-supplied; defaults to T: PartialEq if available +.style(...) // the layout contract, explicit (cached contents aren't + // measured — the same requirement `cached()` hides today, + // view.rs ~262-271) +``` + +- **The `fn` barrier is the soundness argument.** Because the render function + cannot capture, the subtree's inputs are exhaustively: `props` (compared), + ledger reads (tracked), window context (cache-keyed), and boundary dirtiness + (§5). Nothing can be smuggled in. (Non-capturing closures coerce to `fn` + automatically, so the ergonomics stay closure-like.) +- **The stale-closure hazard dissolves as a corollary:** reuse requires props to + compare equal, and every handler constructed inside captured only + props-derived data — so replayed listeners are provably equivalent to fresh + ones. Handlers passed *in* arrive via props as `Rc`s and can be compared by + pointer. +- **Reuse condition:** geometry key matches ∧ read-record clean ∧ boundary not + explicitly invalidated ∧ `compare(prev, next)`. +- **Debuggability flips from objection to feature:** boundaries are visible in + the tree, and each `Cached` can report its read-set, its props diff, and its + last bust reason to the inspector. "Why did this re-render" gets an artifact; + today's answer is grepping for `cx.notify`. + +`Entity::cached` / `AnyView::cached` keep working throughout (an entity view's +self-read makes the read rule equivalent or better). + +### The struct `View` trait after this + +`fn entity_id(&self) -> Option` is removed, replaced by an optional +**element key** for sibling/state disambiguation (`fn key(&self) -> +Option` or equivalent). An `EntityId` remains a perfectly good *key* +— a tab title keyed by its editor's id is using the id as a name, not a mailbox +(role 1 without role 3). `Entity` views are untouched. + +### Projections after this (separable) + +With invalidation read-derived, projections need no backing entities: a +`Projection

` becomes a plain `{ source, composed lens }` value, constructible +anywhere (the current render-context restriction existed only to mint an +identity for it). `observe` delegates to the source; equality-gated fine-grained +observation remains a possible opt-in data-plane tool. Most of `projection.rs` +melts away. The lens-change notify fix on that branch stands on its own +regardless. + +--- + +## 7. Adversarial review: objections and dispositions + +An agent with full code access was instructed to build the strongest case +against this design and rank each objection: fatal / serious-but-mitigable / +friction. Dispositions: + +| # | Objection (severity as reviewed) | Disposition | +|---|---|---| +| 1 | Universal props-compared caching is unsound: hover/active/scroll live in element state, invisible to reads and props; reused subtrees replay stale listeners (fatal as specified) | **Absorbed.** The §5 boundary channel keeps interaction live under caches; the §6 `fn` barrier + props funnel closes the stale-capture hole. Ambient `.cached()`-anywhere is *not* shipped; `Cached` is the only new boundary form. | +| 2 | The ledger can't see paint reads; deferred draws break contiguous ranges; a global index-set mis-attributes sibling reads (fatal as first drafted) | **Conceded and redesigned.** §4 ownership attribution: records not ranges, phases as episodes, deferred draws re-open originator records, O(1) dedup. | +| 3 | `current_view` is load-bearing across the interaction layer; deleting view identity degrades those notifies to huge ancestor entities → bust storms (serious) | **Absorbed.** §5: those call sites want role 2 and now get a real role-2 channel. Large mechanical migration; the objection's site list is the checklist. | +| 4 | `use_keyed_state` doesn't *read* its entity, so forwarder deletion silently breaks components that only touch state in handlers (serious) | **Conceded, one-line fix:** allocation is a dependency; `use_keyed_state` records a ledger read unconditionally. | +| 5 | "The cost is already paid" is overstated: globals are untracked today; per-boundary bookkeeping is new; `refresh()` bypasses caching on many real frames; widely-read entities cap the win (serious/friction) | **Partially conceded.** The broad-dependency tier (§4) bounds fan-out; global tracking is a cheap tag; `refresh()` prevalence is a pre-existing ceiling this makes worth lowering, not a regression. Benefit claims must be re-validated by profiling in Milestones 1–2. | +| 6 | Observer/effect timing semantics must be preserved bit-for-bit (serious) | **Accepted as a constraint.** Milestones keep `Effect::Notify` semantics and draw-phase suppression untouched; read-rule busting is strictly additive in Milestone 1. | +| 7 | Implicit dependency tracking hurts debuggability and GPUI's explicitness ethos (friction) | **Flipped.** Boundaries are explicit components; read-sets are inspectable artifacts. The entity plane stays fully explicit. | +| 8 | A much smaller fix captures most of the benefit (decisive comparison) | **Adopted as sequencing.** The "smaller fix" *is* Milestone 1. Every milestone is a coherent stopping point. | + +**Falsifiers — how we'd know this design is wrong:** Milestone 1 reveals +widespread dependence on forwarder-as-observer semantics beyond the audited +sites; profiling shows ledger/record overhead in typing latency or terminal +repaint; the broad-dependency tier ends up covering most dependencies +(fine-grained buys little in practice); Milestone 2's `current_view` migration +finds a genuine role-3 dependency that cannot be expressed as boundary +invalidation. + +--- + +## 8. Milestones + +1. **Contract swap, additive, behind existing gates** — read-set busting at the + existing entity-backed cache sites; forwarder deletion with + allocation-as-read; loop detection in `flush_effects`. *(Work order in §9.)* +2. **Render-plane channel** — `window.invalidate(boundary)`; migrate the + `current_view` notify sites in `div.rs` / `text.rs` / `list.rs` / `img.rs` / + `window.rs`; stop pushing `Effect::Notify` from the invalidator path for + render-plane invalidations. +3. **`Cached`** — the record-stack bookkeeping (§4), the component (§6), + deferred-draw record re-opening, globals in the ledger. +4. **struct `View` trait surgery & projections-as-values** — remove + `entity_id`, add element keys; collapse `projection.rs` to plain lens values. + +--- + +## 9. Milestone 1 work order (self-contained worker prompt) + +> You are implementing Milestone 1 of GPUI's read-tracked invalidation redesign +> in the Zed repository. Read this section fully, then the cited code, before +> writing anything. The design context is +> `crates/gpui/docs/read_tracked_invalidation.md` (§1–§8); you only need §9 to +> execute, but read §3 and §4 to understand intent, and the terms table at the +> top for vocabulary. Use `./script/clippy` instead of `cargo clippy`. Do not +> commit; leave changes in the working tree. +> +> **Goal:** make subtree cache invalidation consult recorded read-sets +> (additively — never bust less than today), delete the `use_keyed_state` +> forwarder safely, and add effect-loop detection. No public API changes. No +> `View`-trait changes. +> +> ### Task 1 — Expose the per-frame notified-entity set +> `WindowInvalidator::invalidate_view` (`crates/gpui/src/window.rs` ~153) +> accumulates notified entity ids in `dirty_views`; +> `Window::invalidate_entities` (~2947) drains it through `mark_view_dirty` +> (~1936), which ancestor-walks into `Window::dirty_views`. Preserve all of +> that, and additionally retain the **raw notified set** for the frame (before +> path-marking) as e.g. `Window::dirty_entities: FxHashSet`, cleared +> where `dirty_views` is cleared (~2842-2846). +> +> ### Task 2 — Read-set busting at existing cache sites (additive) +> In `ViewElement::prepaint`'s cached branch (`crates/gpui/src/view.rs` +> ~386-401), the reuse condition currently requires +> `!window.dirty_views.contains(&entity_id)`. Add: +> `element_state.accessed_entities.is_disjoint(&window.dirty_entities)`. +> The read-set is already captured at ~405 (`detect_accessed_entities`, +> `crates/gpui/src/app.rs` ~1079) and re-registered on reuse at ~395. Keep the +> identity check — this milestone only ever busts *more*. +> +> ### Task 3 — Allocation is a dependency; delete the forwarder +> In `Window::use_keyed_state` (`crates/gpui/src/window.rs` ~3648): +> (a) on **every** call (creation and lookup), record a read of the state entity +> in the ledger (`cx.entities.accessed_entities`) — components that only touch +> their state inside event handlers must still be treated as depending on it +> (see `crates/settings_ui/src/components/number_field.rs` ~286 for the +> pattern); +> (b) delete the `cx.observe(&new_state, move |_, cx| cx.notify(current_view))` +> forwarder (~3656-3666) and the now-unused `current_view` capture. +> Do **not** change `Effect::Notify` semantics or the draw-phase suppression in +> `invalidate_view` (~157-164); observers of entities must fire exactly as +> today. +> +> ### Task 4 — Effect-loop detection +> In `App::flush_effects` (`crates/gpui/src/app.rs` ~1610), count effects +> processed within one flush; past a large threshold (e.g. 1,000,000), panic +> with a message naming the likely cause (an observe/notify cycle between +> entities) and, in test builds, the most-frequently-notified `EntityId`s. +> Bound, don't alter, semantics. +> +> ### Task 5 — Tests (write these first where practical) +> 1. **Read-busting:** an entity-backed `cached()` view whose subtree reads a +> *different* entity X (not its identity) re-renders when X is notified. +> Must fail before Task 2, pass after. Follow the harness style in +> `crates/gpui/src/projection.rs`'s test module (`HookView` / real +> `window.draw` cycles). +> 2. **Forwarder-deletion safety:** a view whose `use_state` entity is updated +> and notified from an observer/handler (never `.read()` in render beyond +> the allocation) still re-renders. Must pass after Task 3. +> 3. **Loop class dead:** in +> `crates/gpui/examples/view_example/example_tests.rs`, the +> `nested_subforms_do_not_feed_back` test guards a feedback loop that +> previously hung when `Subform::entity_id` returned +> `Some(self.person.entity_id())`. Add a variant with exactly that identity +> and assert it settles (`run_until_parked` returns; values correct). It +> hangs before Task 3 and must pass after. Note the example tests require +> `--features test-support`. +> 4. **Loop detector:** a deliberate two-entity observe/notify cycle panics +> with the diagnostic instead of hanging. +> +> ### Acceptance +> - `cargo test -p gpui` and +> `cargo test -p gpui --features test-support --example view_example` pass. +> - `./script/clippy -p gpui` clean. +> - Run `cargo test -p ui -p workspace` (or the nearest available UI-consuming +> suites); **report** any failures that indicate dependence on forwarder +> observer semantics rather than papering over them — that list is a primary +> deliverable (see §7, falsifiers). +> - Summarize: what busts more often than before (expected: nothing +> user-visible; read-busting is additive), and any `use_state` call sites +> whose behavior you believe changed. + +--- + +## 10. Appendix: the minimal loop (for posterity) + +```text +value ──(clamp subscription: "data changed ⇒ cursor moved, notify editor")──▶ editor + ▲ │ + └──(use_state forwarder: "internals changed ⇒ notify current_view" = value)───┘ +``` + +Every edge is locally downstream *in intent*; the alias (`View::entity_id` +returning the value's id) folds "downstream of the widget" onto "upstream of the +data," and the effect queue never drains. Reproduced on `main` (no projections +involved) via `view_example`'s `Input` over a plain `Entity`: hangs on +the first write. The same topology with a projection's backing entity, or with +the projection's *source* entity, also hangs. Positional identity (`None`) +avoids it at the cost of sibling state collisions — which is what motivated this +redesign instead of the workaround. diff --git a/crates/gpui/examples/view_example/example_editor.rs b/crates/gpui/examples/view_example/example_editor.rs index 2064d7cacef29c..d4795b61283b8d 100644 --- a/crates/gpui/examples/view_example/example_editor.rs +++ b/crates/gpui/examples/view_example/example_editor.rs @@ -1,8 +1,12 @@ //! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard //! handling, and the specialized text-shaping renderer. The *text itself* lives -//! in a shared `Entity` it's handed at construction, so the value is +//! behind a `ProjectionMut` it's handed at construction, so the value is //! readable/writable from outside while the editing machinery stays in here. //! +//! Taking a projection rather than an `Entity` is what lets one form +//! entity back several editors: the caller decides whether the text is a whole +//! entity or one field of a larger struct, and the editor can't tell. +//! //! This is the piece that proves the point: a text input is genuinely //! complicated, and `View` lets all of that complexity live in one entity that //! anything can embed. @@ -12,15 +16,16 @@ use std::time::Duration; use gpui::{ App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable, - InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task, - TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size, + InteractiveElement, LayoutId, PaintQuad, Pixels, ProjectionMut, ShapedLine, SharedString, + Subscription, Task, TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, + relative, size, }; use unicode_segmentation::*; use crate::{Backspace, Delete, End, Home, Left, Right}; pub struct Editor { - pub value: Entity, + pub value: ProjectionMut, pub focus_handle: FocusHandle, pub cursor: usize, pub cursor_visible: bool, @@ -32,12 +37,14 @@ impl Editor { /// An editor that owns its own string internally, seeded with `text`. /// Nothing to allocate or wire up at the call site. pub fn new(text: impl Into, window: &mut Window, cx: &mut Context) -> Self { - let value = cx.new(|_| text.into()); + // A whole entity converts into a projection of itself, so the editor + // below doesn't need a second code path for the owned case. + let value = cx.new(|_| text.into()).into(); Self::over(value, window, cx) } /// An editor over a string *you* own, so the value is shared in and out. - pub fn over(value: Entity, window: &mut Window, cx: &mut Context) -> Self { + pub fn over(value: ProjectionMut, window: &mut Window, cx: &mut Context) -> Self { let focus_handle = cx.focus_handle(); let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| { @@ -52,7 +59,7 @@ impl Editor { // boundary before the next IME round-trip can slice out of bounds, and // (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache // is keyed on *our* notify, not the value's. - let value_sub = cx.observe(&value, |this, value, cx| { + let value_sub = value.observe(cx, |this, value, cx| { let content = value.read(cx); let mut cursor = this.cursor.min(content.len()); while cursor > 0 && !content.is_char_boundary(cursor) { @@ -145,9 +152,8 @@ impl Editor { if self.cursor > 0 { let prev = previous_boundary(&content, self.cursor); let cursor = self.cursor; - self.value.update(cx, |s, cx| { + self.value.update(cx, |s| { s.drain(prev..cursor); - cx.notify(); }); self.cursor = prev; } @@ -160,9 +166,8 @@ impl Editor { if self.cursor < content.len() { let next = next_boundary(&content, self.cursor); let cursor = self.cursor; - self.value.update(cx, |s, cx| { + self.value.update(cx, |s| { s.drain(cursor..next); - cx.notify(); }); } self.reset_blink(cx); @@ -171,10 +176,7 @@ impl Editor { pub fn insert_newline(&mut self, cx: &mut Context) { let cursor = self.cursor; - self.value.update(cx, |s, cx| { - s.insert(cursor, '\n'); - cx.notify(); - }); + self.value.update(cx, |s| s.insert(cursor, '\n')); self.cursor += 1; self.reset_blink(cx); cx.notify(); @@ -289,10 +291,7 @@ impl EntityInputHandler for Editor { let new_content = content[..range.start].to_owned() + new_text + &content[range.end..]; self.cursor = range.start + new_text.len(); - self.value.update(cx, |s, cx| { - *s = new_content; - cx.notify(); - }); + self.value.update(cx, |s| *s = new_content); self.reset_blink(cx); cx.notify(); } diff --git a/crates/gpui/examples/view_example/example_input.rs b/crates/gpui/examples/view_example/example_input.rs index 25d74013deb757..e3445aad3bdd01 100644 --- a/crates/gpui/examples/view_example/example_input.rs +++ b/crates/gpui/examples/view_example/example_input.rs @@ -1,24 +1,24 @@ //! `Input` — a single-line text input. The shaping layer over `Editor`. //! //! Construct it two ways, depending on how much state you want to own: -//! * `Input::new(value: Entity)` — you hold just the string; the input -//! allocates the `Editor` internally via `use_state`. Value readable, cursor hidden. +//! * `Input::new(value: ProjectionMut)` — you hold just the text; the +//! input allocates the `Editor` internally via `use_state`. Value readable, +//! cursor hidden. The text can be a whole `Entity` (via `.into()`) or +//! one field of a bigger struct (via `project!`) — the input can't tell. //! * `Input::editor(editor: Entity)` — you hold the editor; cursor/selection //! are now yours to read and drive too. //! -//! Either way the chrome is identical. Because the string (or editor) is the -//! input's *identity*, the internal `use_state(Editor)` is collision-safe across -//! any number of inputs. +//! Either way the chrome is identical. use gpui::{ - App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, StyleRefinement, - Window, div, hsla, point, prelude::*, px, white, + App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, ProjectionMut, + StyleRefinement, Window, div, hsla, point, prelude::*, px, white, }; use crate::example_editor::{Editor, standard_actions}; enum Source { - Value(Entity), + Value(ProjectionMut), Editor(Entity), } @@ -30,8 +30,8 @@ pub struct Input { } impl Input { - /// Backed by a bare string; the editor is allocated internally. - pub fn new(value: Entity) -> Self { + /// Backed by a projected string; the editor is allocated internally. + pub fn new(value: ProjectionMut) -> Self { Self { source: Source::Value(value), width: None, @@ -61,10 +61,17 @@ impl Input { impl gpui::View for Input { fn entity_id(&self) -> Option { - Some(match &self.source { - Source::Value(value) => value.entity_id(), - Source::Editor(editor) => editor.entity_id(), - }) + match &self.source { + // A view's identity is the notify target for state allocated inside + // it. The editor below is allocated here and observes the value, so + // identifying this view by the value would route the editor's + // notifications back into the thing it observes and spin forever. + // Positional identity is correct here. + Source::Value(_) => None, + // Nothing is allocated in this branch, so the editor we were handed + // is a safe identity and survives moving around the tree. + Source::Editor(editor) => Some(editor.entity_id()), + } } fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { diff --git a/crates/gpui/examples/view_example/example_tests.rs b/crates/gpui/examples/view_example/example_tests.rs index a3edae8cda1f6d..63ef585dfe20df 100644 --- a/crates/gpui/examples/view_example/example_tests.rs +++ b/crates/gpui/examples/view_example/example_tests.rs @@ -6,14 +6,17 @@ #[cfg(test)] mod tests { - use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*}; + use gpui::{ + Context, Entity, IntoElement, KeyBinding, ProjectionMut, TestAppContext, Window, + prelude::*, project, + }; use crate::example_editor::Editor; use crate::example_input::Input; use crate::{Backspace, Delete, End, Home, Left, Right}; /// Two inputs, each backed by an editor we own (so the test can focus and - /// read them). Proves data flows through the shared `String` and that + /// read them). Proves data flows through the projected `String` and that /// sibling inputs stay isolated. struct Harness { a: Entity, @@ -45,15 +48,17 @@ mod tests { cx: &mut TestAppContext, ) -> ( Entity, - Entity, - Entity, + ProjectionMut, + ProjectionMut, &mut gpui::VisualTestContext, ) { bind_keys(cx); let (harness, cx) = cx.add_window_view(|window, cx| { - let a_value = cx.new(|_| String::new()); - let b_value = cx.new(|_| String::new()); + // A whole entity projects to itself, so an editor over an entity and + // an editor over one field of a form are the same thing to `Editor`. + let a_value = cx.new(|_| String::new()).into(); + let b_value = cx.new(|_| String::new()).into(); let a = cx.new(|cx| Editor::over(a_value, window, cx)); let b = cx.new(|cx| Editor::over(b_value, window, cx)); Harness { a, b } @@ -79,7 +84,7 @@ mod tests { cx.simulate_input("hello"); - cx.read_entity(&a_value, |value, _| assert_eq!(value, "hello")); + cx.update(|_, cx| assert_eq!(a_value.read(cx), "hello")); cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5)); } @@ -89,9 +94,13 @@ mod tests { cx.simulate_input("x"); - cx.read_entity(&a_value, |value, _| assert_eq!(value, "x")); - cx.read_entity(&b_value, |value, _| { - assert_eq!(value, "", "typing in input A must not touch input B") + cx.update(|_, cx| { + assert_eq!(a_value.read(cx), "x"); + assert_eq!( + b_value.read(cx), + "", + "typing in input A must not touch input B" + ); }); } @@ -105,14 +114,9 @@ mod tests { // Write the shared value from outside the editor. The old cursor (5) // now points into the middle of a multi-byte character; the editor's // observation must clamp it back onto a boundary. - cx.update(|_, cx| { - a_value.update(cx, |value, cx| { - *value = "日本".to_string(); - cx.notify(); - }) - }); + cx.update(|_, cx| a_value.update(cx, |value| *value = "日本".to_string())); - cx.read_entity(&a_value, |value, _| assert_eq!(value, "日本")); + cx.update(|_, cx| assert_eq!(a_value.read(cx), "日本")); cx.read_entity(&editor, |editor, _| { assert_eq!(editor.cursor, 3, "cursor must clamp to a char boundary"); }); @@ -128,4 +132,78 @@ mod tests { cx.simulate_keystrokes("left left"); cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 1)); } + + /// Guards a feedback loop: a view's identity is the notify target for state + /// allocated inside it, so a subform identified by the projection it also + /// projects from will notify itself forever. This test hangs if that + /// regresses. + #[gpui::test] + fn nested_subforms_do_not_feed_back(cx: &mut TestAppContext) { + let (root, cx) = cx.add_window_view(|_, cx| SubformHarness { + profile: cx.new(|_| Profile { + primary: Person::default(), + secondary: Person::default(), + }), + }); + + let profile = cx.read_entity(&root, |root, _| root.profile.clone()); + + // Writing the source notifies the projections the subforms read, which + // in turn notify the editors allocated inside them. If any of those + // notifications routes back into the projection graph, this never + // settles. + cx.update(|_, cx| { + profile.update(cx, |profile, cx| { + profile.primary.name = "hi".to_string(); + cx.notify(); + }) + }); + cx.run_until_parked(); + + cx.read_entity(&profile, |profile, _| { + assert_eq!(profile.primary.name, "hi"); + assert_eq!(profile.secondary.name, "", "subforms must stay isolated"); + }); + } + + #[derive(Default)] + struct Person { + name: String, + } + + struct Profile { + primary: Person, + secondary: Person, + } + + /// Two instances of one subform over two projected people, mirroring the + /// example's `PersonForm`. + struct SubformHarness { + profile: Entity, + } + + impl Render for SubformHarness { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let primary = project!(window, cx, &self.profile, mut primary); + let secondary = project!(window, cx, &self.profile, mut secondary); + gpui::div() + .child(Subform { person: primary }) + .child(Subform { person: secondary }) + } + } + + #[derive(IntoElement)] + struct Subform { + person: ProjectionMut, + } + + impl gpui::View for Subform { + fn entity_id(&self) -> Option { + None + } + + fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement { + Input::new(project!(window, cx, &self.person, mut name)) + } + } } diff --git a/crates/gpui/examples/view_example/example_text_area.rs b/crates/gpui/examples/view_example/example_text_area.rs index 07640b93294677..cf1586afacdb62 100644 --- a/crates/gpui/examples/view_example/example_text_area.rs +++ b/crates/gpui/examples/view_example/example_text_area.rs @@ -1,17 +1,18 @@ //! `TextArea` — a multi-line text box. Same `Editor` workhorse, taller chrome, //! and `Enter` inserts a newline instead of being ignored. Constructible from a -//! string or an editor, exactly like [`Input`](crate::example_input::Input). +//! projected string or an editor, exactly like +//! [`Input`](crate::example_input::Input). use gpui::{ - App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, StyleRefinement, Window, div, - hsla, point, prelude::*, px, white, + App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, ProjectionMut, + StyleRefinement, Window, div, hsla, point, prelude::*, px, white, }; use crate::Enter; use crate::example_editor::{Editor, standard_actions}; enum Source { - Value(Entity), + Value(ProjectionMut), Editor(Entity), } @@ -23,7 +24,7 @@ pub struct TextArea { } impl TextArea { - pub fn new(value: Entity, rows: usize) -> Self { + pub fn new(value: ProjectionMut, rows: usize) -> Self { Self { source: Source::Value(value), rows, @@ -47,10 +48,12 @@ impl TextArea { impl gpui::View for TextArea { fn entity_id(&self) -> Option { - Some(match &self.source { - Source::Value(value) => value.entity_id(), - Source::Editor(editor) => editor.entity_id(), - }) + match &self.source { + // See `Input::entity_id`: identifying this view by the value it + // allocates an editor over would feed back and spin forever. + Source::Value(_) => None, + Source::Editor(editor) => Some(editor.entity_id()), + } } fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { diff --git a/crates/gpui/examples/view_example/view_example_main.rs b/crates/gpui/examples/view_example/view_example_main.rs index 0eac8494ffc016..fc94880a954765 100644 --- a/crates/gpui/examples/view_example/view_example_main.rs +++ b/crates/gpui/examples/view_example/view_example_main.rs @@ -7,9 +7,16 @@ //! //! * `Editor` — the workhorse entity: cursor, blink, focus, keyboard, and a //! specialized text renderer. All the hard parts live here. -//! * `String` — the data plane. `editor.text(cx)` / `value.read(cx)` get it out. -//! * `Input` / `TextArea` — the shaping layer. Each takes a `String` (and grows -//! the editor internally) OR an `Editor` (so you can read the cursor). +//! * `Projection` — the data plane. One `Profile` entity holds every +//! field; each component gets a projection of the one field it +//! touches, so nothing below needs to know the form exists. +//! * `Input` / `TextArea` — the shaping layer. Each takes a projected string +//! (and grows the editor internally) OR an `Editor` (so you can +//! read the cursor). +//! +//! The projections are built inline in the element tree with `project!`, which +//! is the intended shape: a component is handed the field it edits, not the +//! struct that contains it. //! //! Run: `cargo run -p gpui --example view_example` @@ -25,8 +32,9 @@ use example_input::Input; use example_text_area::TextArea; use gpui::{ - App, Bounds, Context, Div, Entity, IntoElement, KeyBinding, Render, SharedString, Window, - WindowBounds, WindowOptions, actions, div, hsla, prelude::*, px, rgb, size, + App, Bounds, Context, Div, Entity, EntityId, IntoElement, KeyBinding, Projection, + ProjectionMut, Render, SharedString, Window, WindowBounds, WindowOptions, actions, div, hsla, + prelude::*, project, px, rgb, size, }; use gpui_platform::application; @@ -35,6 +43,71 @@ actions!( [Backspace, Delete, Left, Right, Home, End, Enter, Quit] ); +/// The whole form, in one entity. No component below ever receives this — they +/// get [`Projection`]s of individual fields, so a component that edits a name +/// works the same whether the name is a field here or a standalone entity. +struct Profile { + primary: Person, + emergency_contact: Person, + bio: String, +} + +struct Person { + name: String, + email: String, +} + +/// A subform over *a* person, wherever that person lives. It projects `name` and +/// `email` out of a `ProjectionMut`, so the projections it builds are +/// two lenses deep — `Profile` to `Person` to `String` — without this component +/// knowing that, and without copying anything along the way. +#[derive(IntoElement)] +struct PersonForm { + person: ProjectionMut, +} + +impl gpui::View for PersonForm { + fn entity_id(&self) -> Option { + // Deliberately *not* the projection's id. A view's identity becomes the + // notify target for state allocated inside it, so identifying a view by + // a projection it also reads from feeds that state's notifications back + // into the projection graph and spins forever. Positional identity is + // enough here: the two subforms sit at different places in the tree. + None + } + + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap(px(8.)) + .child(Input::new(project!(window, cx, &self.person, mut name)).width(px(280.))) + .child( + Input::new(project!(window, cx, &self.person, mut email)) + .width(px(280.)) + .color(hsla(0., 0., 0.3, 1.)), + ) + } +} + +/// A stateless readout of a projected string, rendered far from the input that +/// writes it: a read-only `Projection` in, no subscription, no wiring. +#[derive(IntoElement)] +struct FieldReadout { + label: &'static str, + value: Projection, +} + +impl gpui::RenderOnce for FieldReadout { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let value = self.value.read(cx); + div() + .text_sm() + .text_color(hsla(0., 0., 0.45, 1.)) + .child(SharedString::from(format!("{}: {value}", self.label))) + } +} + /// A tiny stateless view that reads an editor's cursor and is composed *beside* /// the thing editing it — two views over one entity, zero wiring. #[derive(IntoElement)] @@ -68,10 +141,19 @@ impl ViewExample { impl Render for ViewExample { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // The data plane: plain strings, allocated at the top by the hook. - let name = window.use_state(cx, |_, _| String::new()); - let email = window.use_state(cx, |_, _| String::from("me@example.com")); - let bio = window.use_state(cx, |_, _| String::new()); + // The data plane: one entity for the whole form. Fields are handed out + // below as projections, built inline where they're used. + let profile = window.use_state(cx, |_, _| Profile { + primary: Person { + name: String::new(), + email: String::from("me@example.com"), + }, + emergency_contact: Person { + name: String::new(), + email: String::new(), + }, + bio: String::new(), + }); // Editors that own their own string internally — no extra wiring up top. let notes = window.use_state(cx, |window, cx| Editor::new("multi\nline", window, cx)); let owned = window.use_state(cx, |window, cx| Editor::new("editable", window, cx)); @@ -83,14 +165,35 @@ impl Render for ViewExample { .bg(rgb(0xf0f0f0)) .p(px(24.)) .gap(px(24.)) + // One component, two people, one entity. Each subform is handed a + // projected `Person` and projects further from there. .child( - section("Inputs — from a String (cursor stays internal)") - .child(Input::new(name).width(px(320.))) - .child( - Input::new(email) - .width(px(320.)) - .color(hsla(0., 0., 0.3, 1.)), - ), + section("Subforms — the same component over two projected people").child( + div() + .flex() + .gap(px(16.)) + .child(PersonForm { + person: project!(window, cx, &profile, mut primary), + }) + .child(PersonForm { + person: project!(window, cx, &profile, mut emergency_contact), + }), + ), + ) + // Read-only projections of the very same fields, reached by path + // from the root instead of through the subform. Type above and these + // update, because a projection read during render subscribes the + // reader to its source. + .child( + section("Read-only projections — the same fields, somewhere else") + .child(FieldReadout { + label: "name", + value: project!(window, cx, &profile, primary.name), + }) + .child(FieldReadout { + label: "contact", + value: project!(window, cx, &profile, emergency_contact.name), + }), ) .child( section("Input — from an Editor (read its cursor beside it)").child( @@ -103,8 +206,8 @@ impl Render for ViewExample { ), ) .child( - section("Text areas — from a String, or from an Editor") - .child(TextArea::new(bio, 3)) + section("Text areas — from a projected field, or from an Editor") + .child(TextArea::new(project!(window, cx, &profile, mut bio), 3)) .child( div() .flex() diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index e4e9f3b58a5f73..bc5c10fd9cabdd 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -164,6 +164,20 @@ impl EntityMap { .unwrap_or_else(|| double_lease_panic::("read")) } + /// Read an entity's state through a dynamically typed handle, without + /// cloning the handle to downcast it. Panics if `T` does not match the + /// entity's actual type. + pub fn read_any(&self, entity: &AnyEntity) -> &T { + self.assert_valid_context(entity); + let mut accessed_entities = self.accessed_entities.borrow_mut(); + accessed_entities.insert(entity.entity_id); + + self.entities + .get(entity.entity_id) + .and_then(|entity| entity.downcast_ref()) + .unwrap_or_else(|| double_lease_panic::("read")) + } + fn assert_valid_context(&self, entity: &AnyEntity) { debug_assert!( Weak::ptr_eq(&entity.entity_map, &Arc::downgrade(&self.ref_counts)), @@ -1182,9 +1196,153 @@ impl fmt::Debug for BacktraceFormatter { } } +/// A strong, read-only handle to an entity of type `T`. +/// +/// Entities buy back shared mutability from the borrow checker, but in doing +/// so every handle becomes a write handle. A `ReadEntity` restores the +/// read/write distinction at the capability level: holders can read the state +/// (and thereby be re-rendered when it changes), but cannot update or notify +/// it. This makes the set of possible writers of an entity exactly the set of +/// `Entity` holders, which is auditable in a way "everyone" is not. +/// +/// Like [`Entity`], this is a strong handle: it keeps the underlying entity +/// alive. Use [`ReadEntity::downgrade`] where that would create a cycle. +pub struct ReadEntity { + entity: Entity, +} + +impl ReadEntity { + /// Read the entity's state. + pub fn read<'a>(&self, cx: &'a App) -> &'a T { + self.entity.read(cx) + } + + /// Read the entity's state with the given function. + pub fn read_with(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> R { + self.entity.read_with(cx, read) + } + + /// The id of the underlying entity. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } + + /// Convert this handle into a weak variant, which does not keep the + /// underlying entity alive. + pub fn downgrade(&self) -> WeakReadEntity { + WeakReadEntity { + entity: self.entity.downgrade(), + } + } +} + +impl Clone for ReadEntity { + fn clone(&self) -> Self { + Self { + entity: self.entity.clone(), + } + } +} + +impl std::fmt::Debug for ReadEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ReadEntity") + .field("entity_id", &self.entity.entity_id()) + .finish_non_exhaustive() + } +} + +impl PartialEq for ReadEntity { + fn eq(&self, other: &Self) -> bool { + self.entity == other.entity + } +} + +impl Eq for ReadEntity {} + +impl Hash for ReadEntity { + fn hash(&self, state: &mut H) { + self.entity.hash(state); + } +} + +impl Entity { + /// A read-only handle to this entity. + pub fn read_only(&self) -> ReadEntity { + ReadEntity { + entity: self.clone(), + } + } +} + +impl From> for ReadEntity { + fn from(entity: Entity) -> Self { + ReadEntity { entity } + } +} + +impl From> for crate::Projection { + fn from(read_entity: ReadEntity) -> Self { + read_entity.entity.into() + } +} + +/// A weak variant of [`ReadEntity`] which does not keep the underlying entity +/// alive. +pub struct WeakReadEntity { + entity: WeakEntity, +} + +impl WeakReadEntity { + /// The id of the underlying entity. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } + + /// Upgrade to a strong read-only handle. Returns `None` if the underlying + /// entity has been released. + pub fn upgrade(&self) -> Option> { + Some(ReadEntity { + entity: self.entity.upgrade()?, + }) + } + + /// Read the entity's state with the given function, if the entity still + /// exists. + pub fn read_with( + &self, + cx: &C, + read: impl FnOnce(&T, &App) -> R, + ) -> Result { + self.entity.read_with(cx, read) + } +} + +impl Clone for WeakReadEntity { + fn clone(&self) -> Self { + Self { + entity: self.entity.clone(), + } + } +} + +impl std::fmt::Debug for WeakReadEntity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WeakReadEntity") + .field("entity_id", &self.entity.entity_id()) + .finish_non_exhaustive() + } +} + +impl From> for WeakReadEntity { + fn from(entity: WeakEntity) -> Self { + WeakReadEntity { entity } + } +} + #[cfg(test)] mod test { - use crate::EntityMap; + use crate::{AppContext as _, EntityMap, Projection, ReadEntity, TestAppContext}; struct TestEntity { pub i: i32, @@ -1275,4 +1433,54 @@ mod test { drop(pre_existing); drop(leaked); } + + #[test] + fn read_entity_reads_and_converts() { + let mut cx = TestAppContext::single(); + let value = cx.update(|cx| cx.new(|_| "hello".to_string())); + + let read_only = value.read_only(); + let converted: ReadEntity = value.clone().into(); + let projected: Projection = read_only.clone().into(); + + cx.update(|cx| { + assert_eq!(read_only.read(cx), "hello"); + assert_eq!(converted.read(cx), "hello"); + assert_eq!(projected.read(cx), "hello"); + assert_eq!( + read_only.read_with(cx, |value, _| value.len()), + "hello".len() + ); + assert_eq!(read_only.entity_id(), value.entity_id()); + assert_eq!(read_only, converted); + }); + } + + #[test] + fn weak_read_entities_do_not_keep_the_entity_alive() { + let mut cx = TestAppContext::single(); + let value = cx.update(|cx| cx.new(|_| "hello".to_string())); + + let read_only = value.read_only(); + let weak = read_only.downgrade(); + + { + let upgraded = weak.upgrade().expect("entity is alive"); + cx.update(|cx| assert_eq!(upgraded.read(cx), "hello")); + } + + cx.update(|cx| { + assert_eq!( + weak.read_with(cx, |value, _| value.clone()).ok(), + Some("hello".to_string()) + ); + }); + + drop(value); + drop(read_only); + cx.update(|_| {}); + + assert!(weak.upgrade().is_none()); + cx.update(|cx| assert!(weak.read_with(cx, |value, _| value.clone()).is_err())); + } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 4b1679cbe440fa..d37deea359e4b2 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -36,6 +36,7 @@ mod platform; pub mod prelude; /// Profiling utilities for task, frame, and thread performance tracking. pub mod profiler; +mod projection; #[cfg(any( test, target_os = "windows", @@ -139,6 +140,7 @@ pub use keymap::*; pub use path_builder::*; pub use platform::*; pub use profiler::*; +pub use projection::*; #[cfg(any(target_os = "windows", target_os = "linux", target_family = "wasm"))] pub use queue::{PriorityQueueReceiver, PriorityQueueSender}; pub use refineable::*; diff --git a/crates/gpui/src/projection.rs b/crates/gpui/src/projection.rs new file mode 100644 index 00000000000000..f153701fff716a --- /dev/null +++ b/crates/gpui/src/projection.rs @@ -0,0 +1,1182 @@ +use crate::{ + AnyEntity, AnyWeakEntity, App, Context, ElementId, Entity, EntityId, Subscription, Window, +}; + +type ReadFn

= for<'a> fn(&AnyEntity, &'a App) -> &'a P; +type WriteFn

= fn(&AnyEntity, &mut App, &mut dyn FnMut(&mut P)); + +fn read_entity<'a, P: 'static>(entity: &AnyEntity, cx: &'a App) -> &'a P { + cx.entities.read_any(entity) +} + +fn write_entity(entity: &AnyEntity, cx: &mut App, update: &mut dyn FnMut(&mut P)) { + let entity = match entity.clone().downcast::

() { + Ok(entity) => entity, + Err(_) => unreachable!("an identity projection always stores an entity of its value type"), + }; + entity.update(cx, |state, cx| { + update(state); + cx.notify(); + }); +} + +/// Something a projection can be built from: an entity, or another projection. +/// +/// Projecting from a projection composes paths rather than copying values, so +/// `Profile -> Person -> String` stores one string and walks two lenses to +/// reach it. A component handed a `ProjectionMut` can project its +/// fields without knowing whether the person is a whole entity or a field of +/// something larger. +pub trait ProjectionSource { + /// A read-only handle to the source value. + fn to_projection(&self) -> Projection; +} + +/// A [`ProjectionSource`] that can also be written through. Read-only sources +/// deliberately don't implement this: you can't derive a writable projection +/// from a value you may only read. +pub trait ProjectionSourceMut: ProjectionSource { + /// A writable handle to the source value. + fn to_projection_mut(&self) -> ProjectionMut; +} + +impl ProjectionSource for Entity { + fn to_projection(&self) -> Projection { + self.clone().into() + } +} + +impl ProjectionSourceMut for Entity { + fn to_projection_mut(&self) -> ProjectionMut { + self.clone().into() + } +} + +impl ProjectionSource for Projection { + fn to_projection(&self) -> Projection { + self.clone() + } +} + +impl ProjectionSource for ProjectionMut { + fn to_projection(&self) -> Projection { + self.read_only() + } +} + +impl ProjectionSourceMut for ProjectionMut { + fn to_projection_mut(&self) -> ProjectionMut { + self.clone() + } +} + +struct ReadProjectionState { + source: Projection, + lens: for<'a> fn(&'a E) -> &'a P, + _subscription: Subscription, +} + +impl ReadProjectionState { + fn new( + source: &impl ProjectionSource, + lens: for<'a> fn(&'a E) -> &'a P, + cx: &mut Context, + ) -> Self { + let source = source.to_projection(); + Self { + _subscription: source.observe(cx, |_, _, cx| cx.notify()), + source, + lens, + } + } + + fn update_source( + &mut self, + source: &impl ProjectionSource, + lens: for<'a> fn(&'a E) -> &'a P, + cx: &mut Context, + ) { + let source = source.to_projection(); + let source_changed = self.source.entity_id() != source.entity_id(); + // Best-effort comparison: the same lens body written at two call sites + // may compare unequal, which only costs a spurious notify. A lens + // literal evaluated at one call site has a stable address, so + // re-renders that pass the same lens don't notify. + let lens_changed = !std::ptr::fn_addr_eq(self.lens, lens); + if source_changed { + self._subscription = source.observe(cx, |_, _, cx| cx.notify()); + self.source = source; + } + self.lens = lens; + if source_changed || lens_changed { + // The projected value is now read from a different source or + // through a different lens, so views that read this projection + // last frame must re-render even though the source never notified. + cx.notify(); + } + } + + fn get<'a>(&self, cx: &'a App) -> &'a P { + (self.lens)(self.source.read(cx)) + } +} + +fn read_projection<'a, E: 'static, P: ?Sized + 'static>(entity: &AnyEntity, cx: &'a App) -> &'a P { + cx.entities + .read_any::>(entity) + .get(cx) +} + +struct MutableProjectionState { + read: ReadProjectionState, + /// The same source as `read.source`, kept separately because writing needs + /// the writable handle. Both are replaced together in `update_source`. + source: ProjectionMut, + write: for<'a> fn(&'a mut E) -> &'a mut P, +} + +impl MutableProjectionState { + fn new( + source: &impl ProjectionSourceMut, + read: for<'a> fn(&'a E) -> &'a P, + write: for<'a> fn(&'a mut E) -> &'a mut P, + cx: &mut Context, + ) -> Self { + Self { + read: ReadProjectionState::new(source, read, cx), + source: source.to_projection_mut(), + write, + } + } + + fn update_source( + &mut self, + source: &impl ProjectionSourceMut, + read: for<'a> fn(&'a E) -> &'a P, + write: for<'a> fn(&'a mut E) -> &'a mut P, + cx: &mut Context, + ) { + self.read.update_source(source, read, cx); + self.source = source.to_projection_mut(); + self.write = write; + } +} + +fn read_mutable_projection<'a, E: 'static, P: ?Sized + 'static>( + entity: &AnyEntity, + cx: &'a App, +) -> &'a P { + cx.entities + .read_any::>(entity) + .read + .get(cx) +} + +fn write_projection( + entity: &AnyEntity, + cx: &mut App, + update: &mut dyn FnMut(&mut P), +) { + let (source, write) = { + let state = cx.entities.read_any::>(entity); + (state.source.clone(), state.write) + }; + // Writes walk the path down to the entity that actually owns the value, + // which is the only place the data lives and the only thing notified. + source.update(cx, |value| update(write(value))); +} + +/// A read-only handle to a value `P` projected out of an entity. +/// +/// Projections erase their source: a `Projection` may be backed by an +/// `Entity` or by a lens into a field of some larger entity, and the +/// holder can't tell the difference. This makes them the right parameter type +/// for components that need to *read* state without dictating how the caller +/// stores it. +/// +/// Projections are created during render, via [`Window::use_projection`] and +/// friends (or by converting an [`Entity`] with `From`). There is no way to +/// construct a lens projection outside a render context: a projection's +/// identity comes from its render call site, and state that needs an identity +/// independent of any view should be a proper entity instead. +/// +/// Projections are strong handles: holding one keeps the source entity alive, +/// so reads are infallible. Use [`Projection::downgrade`] where that would +/// create a cycle. +/// +/// Reads are access-tracked just like direct entity reads, so a view that +/// reads a projection during render is re-rendered when the source entity +/// notifies. +/// +/// Note that notifications are only as fine-grained as the source entity: a +/// projection into a frequently-notified entity re-renders its readers on +/// every notification, whether or not the projected value changed. If that +/// becomes a problem, restructure the state so the projected value lives in +/// its own entity, and project from that. +pub struct Projection { + entity: AnyEntity, + read: ReadFn

, +} + +impl Clone for Projection

{ + fn clone(&self) -> Self { + Self { + entity: self.entity.clone(), + read: self.read, + } + } +} + +impl Projection

{ + /// Read the projected value. + pub fn read<'a>(&self, cx: &'a App) -> &'a P { + (self.read)(&self.entity, cx) + } + + /// This projection's identity: the backing entity of the `use_projection` + /// call site that created it, or the source entity for identity conversions + /// from [`Entity`]. Notifications for the projected value are delivered as + /// notifications of this entity. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } + + /// Convert this projection into a weak variant, which does not keep its + /// backing state alive. + pub fn downgrade(&self) -> WeakProjection

{ + WeakProjection { + entity: self.entity.downgrade(), + read: self.read, + } + } + + /// Arranges for `on_notify` to be called whenever the projected value may + /// have changed, i.e. whenever this projection's identity notifies. + /// + /// Writes notify the source entity and the backing state forwards that, so + /// this fires both for writes made through a [`ProjectionMut`] and for + /// writes made directly to the source. Like reads, it is no more + /// fine-grained than the source: an unrelated change to the source entity + /// still notifies. + pub fn observe( + &self, + cx: &mut Context, + mut on_notify: impl FnMut(&mut T, Projection

, &mut Context) + 'static, + ) -> Subscription { + let observer = cx.weak_entity(); + let projection = self.downgrade(); + cx.new_observer( + self.entity_id(), + Box::new(move |cx| { + let (Some(observer), Some(projection)) = (observer.upgrade(), projection.upgrade()) + else { + return false; + }; + observer.update(cx, |observer, cx| on_notify(observer, projection, cx)); + true + }), + ) + } +} + +impl std::fmt::Debug for Projection

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Projection") + .field("entity_id", &self.entity.entity_id()) + .finish_non_exhaustive() + } +} + +/// A read-write handle to a value `P` projected out of an entity. +/// +/// Like [`Projection`], but writable: updates are applied through the lens to +/// the source entity, which is then notified. See [`Window::use_projection_mut`]. +pub struct ProjectionMut { + read: Projection

, + write: WriteFn

, +} + +impl Clone for ProjectionMut

{ + fn clone(&self) -> Self { + Self { + read: self.read.clone(), + write: self.write, + } + } +} + +impl ProjectionMut

{ + /// Read the projected value. + pub fn read<'a>(&self, cx: &'a App) -> &'a P { + self.read.read(cx) + } + + /// This projection's identity. See [`Projection::entity_id`]. + pub fn entity_id(&self) -> EntityId { + self.read.entity_id() + } + + /// Update the projected value, notifying the source entity. + /// + /// Unlike [`Entity::update`], this always notifies: a holder of a + /// `ProjectionMut` has no other way to signal that the state changed, so + /// every write is treated as a change. + /// + /// The usual entity update rules apply: calling this while the source + /// entity is already being updated will panic. + pub fn update(&self, cx: &mut App, f: impl FnOnce(&mut P) -> R) -> R { + let mut f = Some(f); + let mut result = None; + (self.write)(&self.read.entity, cx, &mut |value| { + if let Some(f) = f.take() { + result = Some(f(value)); + } + }); + result.expect("the projection's write function must invoke the callback exactly once") + } + + /// A read-only projection of the same value. + pub fn read_only(&self) -> Projection

{ + self.read.clone() + } + + /// Convert this projection into a weak variant, which does not keep its + /// backing state alive. + pub fn downgrade(&self) -> WeakProjectionMut

{ + WeakProjectionMut { + read: self.read.downgrade(), + write: self.write, + } + } + + /// See [`Projection::observe`]. The callback receives a read-only handle; + /// writing to the value being observed would re-enter the notification. + pub fn observe( + &self, + cx: &mut Context, + on_notify: impl FnMut(&mut T, Projection

, &mut Context) + 'static, + ) -> Subscription { + self.read.observe(cx, on_notify) + } +} + +impl std::fmt::Debug for ProjectionMut

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProjectionMut") + .field("entity_id", &self.read.entity.entity_id()) + .finish_non_exhaustive() + } +} + +/// A weak variant of [`Projection`] which does not keep its backing state +/// alive. Upgrade it to read. +pub struct WeakProjection { + entity: AnyWeakEntity, + read: ReadFn

, +} + +impl Clone for WeakProjection

{ + fn clone(&self) -> Self { + Self { + entity: self.entity.clone(), + read: self.read, + } + } +} + +impl WeakProjection

{ + /// This projection's identity. See [`Projection::entity_id`]. + pub fn entity_id(&self) -> EntityId { + self.entity.entity_id() + } + + /// Upgrade to a strong projection. Returns `None` if the backing state has + /// been released. + pub fn upgrade(&self) -> Option> { + Some(Projection { + entity: self.entity.upgrade()?, + read: self.read, + }) + } +} + +impl std::fmt::Debug for WeakProjection

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WeakProjection") + .field("entity_id", &self.entity.entity_id()) + .finish_non_exhaustive() + } +} + +/// A weak variant of [`ProjectionMut`] which does not keep its backing state +/// alive. Upgrade it to read or write. +pub struct WeakProjectionMut { + read: WeakProjection

, + write: WriteFn

, +} + +impl Clone for WeakProjectionMut

{ + fn clone(&self) -> Self { + Self { + read: self.read.clone(), + write: self.write, + } + } +} + +impl WeakProjectionMut

{ + /// This projection's identity. See [`Projection::entity_id`]. + pub fn entity_id(&self) -> EntityId { + self.read.entity_id() + } + + /// Upgrade to a strong projection. Returns `None` if the backing state has + /// been released. + pub fn upgrade(&self) -> Option> { + Some(ProjectionMut { + read: self.read.upgrade()?, + write: self.write, + }) + } +} + +impl std::fmt::Debug for WeakProjectionMut

{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WeakProjectionMut") + .field("entity_id", &self.read.entity.entity_id()) + .finish_non_exhaustive() + } +} + +impl Window { + /// Use a read-only projection of part of an entity's state. Must be called + /// during render. + /// + /// The lens must be a plain function (closures that capture nothing coerce + /// automatically): + /// + /// ```ignore + /// let name: Projection = window.use_projection(cx, &person, |person| &person.name); + /// ``` + /// + /// The projection's backing state is memoized per call site, like + /// [`Window::use_state`], so sibling projections of different fields of one + /// entity don't collide. When rendering multiple projections from the same + /// location (e.g. in a loop), use [`Window::use_keyed_projection`]. + #[track_caller] + pub fn use_projection( + &mut self, + cx: &mut App, + source: &impl ProjectionSource, + lens: for<'a> fn(&'a E) -> &'a P, + ) -> Projection

{ + self.use_keyed_projection( + ElementId::CodeLocation(*core::panic::Location::caller()), + cx, + source, + lens, + ) + } + + /// Like [`Window::use_projection`], with an explicit key to disambiguate + /// call sites that render multiple times (e.g. in a loop). + pub fn use_keyed_projection( + &mut self, + key: impl Into, + cx: &mut App, + source: &impl ProjectionSource, + lens: for<'a> fn(&'a E) -> &'a P, + ) -> Projection

{ + let state = + self.use_keyed_state(key, cx, |_, cx| ReadProjectionState::new(source, lens, cx)); + state.update(cx, |state, cx| state.update_source(source, lens, cx)); + Projection { + entity: state.into_any(), + read: read_projection::, + } + } + + /// Use a read-write projection of part of an entity's state. Must be + /// called during render. See [`Window::use_projection`]. + /// + /// Takes two lenses because reads only have shared access to the entity + /// while writes have exclusive access; they should address the same value. + /// The [`crate::project!`] macro writes both from a single field path. + #[track_caller] + pub fn use_projection_mut( + &mut self, + cx: &mut App, + source: &impl ProjectionSourceMut, + read: for<'a> fn(&'a E) -> &'a P, + write: for<'a> fn(&'a mut E) -> &'a mut P, + ) -> ProjectionMut

{ + self.use_keyed_projection_mut( + ElementId::CodeLocation(*core::panic::Location::caller()), + cx, + source, + read, + write, + ) + } + + /// Like [`Window::use_projection_mut`], with an explicit key to + /// disambiguate call sites that render multiple times (e.g. in a loop). + pub fn use_keyed_projection_mut( + &mut self, + key: impl Into, + cx: &mut App, + source: &impl ProjectionSourceMut, + read: for<'a> fn(&'a E) -> &'a P, + write: for<'a> fn(&'a mut E) -> &'a mut P, + ) -> ProjectionMut

{ + let state = self.use_keyed_state(key, cx, |_, cx| { + MutableProjectionState::new(source, read, write, cx) + }); + state.update(cx, |state, cx| state.update_source(source, read, write, cx)); + ProjectionMut { + read: Projection { + entity: state.into_any(), + read: read_mutable_projection::, + }, + write: write_projection::, + } + } +} + +/// Use a projection of an entity field, writing the lenses from a single field +/// path. Must be called during render. +/// +/// Read-only by default; prefix the path with `mut` for a writable projection. +/// +/// ```ignore +/// let name: Projection = project!(window, cx, &person, name); +/// let name: ProjectionMut = project!(window, cx, &person, mut name); +/// let city: ProjectionMut = project!(window, cx, &person, mut address.city); +/// ``` +/// +/// Expands to [`Window::use_projection`] with `|state| &state.`, or, with +/// `mut`, to [`Window::use_projection_mut`] with `|state| &mut state.` as +/// the second lens. +#[macro_export] +macro_rules! project { + ($window:expr, $cx:expr, $entity:expr, mut $($field:ident).+) => { + $window.use_projection_mut( + $cx, + $entity, + |state| &state.$($field).+, + |state| &mut state.$($field).+, + ) + }; + ($window:expr, $cx:expr, $entity:expr, $($field:ident).+) => { + $window.use_projection($cx, $entity, |state| &state.$($field).+) + }; +} + +impl From> for Projection

{ + fn from(entity: Entity

) -> Self { + Self { + entity: entity.into_any(), + read: read_entity::

, + } + } +} + +impl From> for ProjectionMut

{ + fn from(entity: Entity

) -> Self { + Self { + read: Projection { + entity: entity.into_any(), + read: read_entity::

, + }, + write: write_entity::

, + } + } +} + +impl From> for Projection

{ + fn from(projection: ProjectionMut

) -> Self { + projection.read + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AppContext as _, IntoElement, Render, TestAppContext, WindowHandle, div}; + use std::{ + cell::{Cell, RefCell}, + rc::Rc, + }; + + struct Person { + name: String, + age: u32, + } + + struct Company { + owner: Person, + } + + // Named lenses so tests that assert "an unchanged lens must not notify" + // pass pointer-identical functions, the way a single render call site does. + // Two closure literals with the same body are not guaranteed to compare + // equal. + fn read_name(person: &Person) -> &String { + &person.name + } + + fn write_name(person: &mut Person) -> &mut String { + &mut person.name + } + + /// Runs `hook` during render so tests build projections the way callers do, + /// through the `use_projection` hooks, and records what each frame produced. + /// + /// `source` and `enabled` are fields rather than captures so tests can swap + /// the projected entity or stop rendering the hook between frames. + struct HookView { + source: Entity, + hook: fn(&mut Window, &mut Context, &Entity) -> H, + enabled: bool, + frames: Rc>>, + } + + impl Render for HookView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if self.enabled { + let produced = (self.hook)(window, cx, &self.source); + self.frames.borrow_mut().push(produced); + } + div() + } + } + + /// Opens a window around [`HookView`] and draws one frame. + fn hook_window( + cx: &mut TestAppContext, + source: &Entity, + hook: fn(&mut Window, &mut Context>, &Entity) -> H, + ) -> (WindowHandle>, Rc>>) { + let frames = Rc::new(RefCell::new(Vec::new())); + let window = cx.add_window({ + let source = source.clone(); + let frames = frames.clone(); + move |_, _| HookView { + source, + hook, + enabled: true, + frames, + } + }); + draw(cx, window); + (window, frames) + } + + fn draw(cx: &mut TestAppContext, window: WindowHandle>) { + cx.update_window(window.into(), |_, window, cx| window.draw(cx).clear(cx)) + .unwrap(); + } + + /// Takes the most recent frame's output and forgets the rest. + /// + /// The recording holds strong handles, so tests that assert something gets + /// dropped must clear anything later frames record as well. + fn take_output(frames: &Rc>>) -> H { + let mut frames = frames.borrow_mut(); + let last = frames.pop().expect("the hook ran during render"); + frames.clear(); + last + } + + /// Draws one frame and returns the projection the hook created. + fn render_projection( + cx: &mut TestAppContext, + source: &Entity, + hook: fn(&mut Window, &mut Context>, &Entity) -> H, + ) -> H { + let (_window, frames) = hook_window(cx, source, hook); + take_output(&frames) + } + + #[test] + fn projection_reads_and_writes_through_the_lens() { + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + + let name = render_projection(&mut cx, &person, |window, cx, person| { + crate::project!(window, cx, person, mut name) + }); + + cx.update(|cx| { + assert_eq!(name.read(cx), "Ada"); + name.update(cx, |name| name.push_str(" Lovelace")); + assert_eq!(name.read(cx), "Ada Lovelace"); + assert_eq!(person.read(cx).name, "Ada Lovelace"); + assert_eq!(person.read(cx).age, 36); + }); + } + + #[test] + fn writes_through_a_projection_notify_the_source_entity() { + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let age = render_projection(&mut cx, &person, |window, cx, person| { + crate::project!(window, cx, person, mut age) + }); + + let notified = Rc::new(Cell::new(0)); + let _subscription = cx.update(|cx| { + cx.observe(&person, { + let notified = notified.clone(); + move |_, _| notified.set(notified.get() + 1) + }) + }); + + cx.update(|cx| age.update(cx, |age| *age += 1)); + + assert_eq!(notified.get(), 1); + cx.update(|cx| assert_eq!(*age.read(cx), 37)); + } + + #[test] + fn entities_convert_to_projections() { + let mut cx = TestAppContext::single(); + let value = cx.update(|cx| cx.new(|_| "hello".to_string())); + + let read_write: ProjectionMut = value.clone().into(); + let read_only: Projection = value.clone().into(); + let downgraded: Projection = read_write.clone().into(); + + cx.update(|cx| { + read_write.update(cx, |value| value.push_str(" world")); + assert_eq!(read_only.read(cx), "hello world"); + assert_eq!(downgraded.read(cx), "hello world"); + assert_eq!(read_only.entity_id(), value.entity_id()); + }); + } + + #[test] + fn clones_share_the_same_source() { + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + + let name = render_projection(&mut cx, &person, |window, cx, person| { + crate::project!(window, cx, person, mut name) + }); + let name_clone = name.clone(); + + cx.update(|cx| { + name.update(cx, |name| *name = "Grace".to_string()); + assert_eq!(name_clone.read(cx), "Grace"); + }); + } + + #[test] + fn projections_can_be_unsized() { + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + + let name: Projection = render_projection(&mut cx, &person, |window, cx, person| { + window.use_projection(cx, person, |person| person.name.as_str()) + }); + + cx.update(|cx| assert_eq!(name.read(cx), "Ada")); + } + + #[test] + fn use_projection_assigns_stable_distinct_identities() { + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let person_id = person.entity_id(); + + // Each frame records both projections plus what they read during that + // render, so later assertions can check identity stability and that + // writes are visible to subsequent frames. + let (window, frames) = hook_window(&mut cx, &person, |window, cx, person| { + let name = crate::project!(window, cx, person, mut name); + let age = crate::project!(window, cx, person, age); + let read = (name.read(cx).clone(), *age.read(cx)); + (name, age, read) + }); + draw(&mut cx, window); + + let name_projection = frames.borrow().last().expect("render ran").0.clone(); + cx.update(|cx| name_projection.update(cx, |name| name.push_str(" Lovelace"))); + draw(&mut cx, window); + + let frames = frames.borrow(); + assert!(frames.len() >= 3); + let identities: Vec<_> = frames + .iter() + .map(|(name, age, _)| (name.entity_id(), age.entity_id())) + .collect(); + let first = identities[0]; + assert!( + identities.iter().all(|frame| *frame == first), + "identities must be stable across frames: {identities:?}" + ); + let (name_id, age_id) = first; + assert_ne!(name_id, age_id, "call sites must have distinct identities"); + assert_ne!(name_id, person_id, "identity must differ from the source"); + assert_ne!(age_id, person_id, "identity must differ from the source"); + assert_eq!( + frames.last().map(|(_, _, read)| (read.0.as_str(), read.1)), + Some(("Ada Lovelace", 36)), + "writes through the projection must be visible to later renders" + ); + } + + #[test] + fn keyed_projection_updates_escaped_handles() { + let mut cx = TestAppContext::single(); + let first = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let second = cx.update(|cx| { + cx.new(|_| Person { + name: "Grace".to_string(), + age: 37, + }) + }); + let (window, frames) = hook_window(&mut cx, &first, |window, cx, person| { + window.use_keyed_projection_mut( + "name", + cx, + person, + |person| &person.name, + |person| &mut person.name, + ) + }); + let escaped = take_output(&frames); + let identity = escaped.entity_id(); + + window + .update(&mut cx, |view, _, cx| { + view.source = second.clone(); + cx.notify(); + }) + .unwrap(); + draw(&mut cx, window); + + cx.update(|cx| { + assert_eq!(escaped.entity_id(), identity); + assert_eq!(escaped.read(cx), "Grace"); + escaped.update(cx, |name| name.push_str(" Hopper")); + assert_eq!(second.read(cx).name, "Grace Hopper"); + assert_eq!(first.read(cx).name, "Ada"); + }); + } + + #[test] + fn swapping_the_source_notifies_and_replaces_the_subscription() { + let mut cx = TestAppContext::single(); + let first = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let second = cx.update(|cx| { + cx.new(|_| Person { + name: "Grace".to_string(), + age: 37, + }) + }); + let state = cx.update(|cx| { + cx.new(|cx| MutableProjectionState::new(&first, read_name, write_name, cx)) + }); + let notifications = Rc::new(Cell::new(0)); + let _subscription = cx.update(|cx| { + cx.observe(&state, { + let notifications = notifications.clone(); + move |_, _| notifications.set(notifications.get() + 1) + }) + }); + + cx.update(|cx| { + state.update(cx, |state, cx| { + state.update_source(&first, read_name, write_name, cx) + }); + }); + assert_eq!( + notifications.get(), + 0, + "an unchanged source should not notify" + ); + + cx.update(|cx| { + state.update(cx, |state, cx| { + state.update_source(&second, read_name, write_name, cx) + }); + }); + assert_eq!(notifications.get(), 1, "swapping the source should notify"); + + cx.update(|cx| first.update(cx, |_, cx| cx.notify())); + assert_eq!( + notifications.get(), + 1, + "the old source's subscription should be dropped" + ); + + cx.update(|cx| second.update(cx, |_, cx| cx.notify())); + assert_eq!(notifications.get(), 2); + } + + #[test] + fn changing_only_the_lens_notifies() { + fn name_lens(person: &Person) -> &str { + &person.name + } + fn fixed_lens(_person: &Person) -> &str { + "fixed" + } + + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let state = cx.update(|cx| cx.new(|cx| ReadProjectionState::new(&person, name_lens, cx))); + let notifications = Rc::new(Cell::new(0)); + let _subscription = cx.update(|cx| { + cx.observe(&state, { + let notifications = notifications.clone(); + move |_, _| notifications.set(notifications.get() + 1) + }) + }); + + cx.update(|cx| { + state.update(cx, |state, cx| state.update_source(&person, name_lens, cx)); + }); + assert_eq!( + notifications.get(), + 0, + "an unchanged source and lens should not notify" + ); + + // The source entity is untouched, but the projected value is different: + // holders of an escaped handle must still be told. + cx.update(|cx| { + state.update(cx, |state, cx| state.update_source(&person, fixed_lens, cx)); + }); + assert_eq!( + notifications.get(), + 1, + "a new lens projects a different value and must notify" + ); + cx.update(|cx| assert_eq!(state.read(cx).get(cx), "fixed")); + } + + #[test] + fn escaped_projection_keeps_its_backing_state_alive() { + let mut cx = TestAppContext::single(); + let source = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let (window, frames) = hook_window(&mut cx, &source, |window, cx, person| { + crate::project!(window, cx, person, mut name) + }); + let escaped = take_output(&frames); + + window + .update(&mut cx, |view, _, cx| { + view.enabled = false; + cx.notify(); + }) + .unwrap(); + draw(&mut cx, window); + drop(source); + cx.update(|_| {}); + + cx.update(|cx| { + escaped.update(cx, |name| name.push_str(" Lovelace")); + assert_eq!(escaped.read(cx), "Ada Lovelace"); + }); + } + + #[test] + fn projections_compose_through_a_projected_source() { + struct Watcher { + notifications: usize, + _subscription: Option, + } + + let mut cx = TestAppContext::single(); + let company = cx.update(|cx| { + cx.new(|_| Company { + owner: Person { + name: "Ada".to_string(), + age: 36, + }, + }) + }); + + // Two lenses deep: the second projection's source is the first + // projection, not an entity. + let name = render_projection(&mut cx, &company, |window, cx, company| { + let owner = crate::project!(window, cx, company, mut owner); + crate::project!(window, cx, &owner, mut name) + }); + + let watcher = cx.update(|cx| { + cx.new(|cx| Watcher { + notifications: 0, + _subscription: Some( + name.observe(cx, |watcher: &mut Watcher, _, _| watcher.notifications += 1), + ), + }) + }); + + cx.update(|cx| { + assert_eq!(name.read(cx), "Ada"); + // The write walks both lenses down to the root entity; there is no + // copy of the string at either intermediate level. + name.update(cx, |name| name.push_str(" Lovelace")); + assert_eq!(company.read(cx).owner.name, "Ada Lovelace"); + }); + + // A write straight to the root has to climb back up through both + // levels to reach anything observing the leaf. + cx.update(|cx| { + company.update(cx, |company, cx| { + company.owner.name = "Grace".to_string(); + cx.notify(); + }) + }); + + cx.update(|cx| { + assert_eq!(name.read(cx), "Grace"); + assert_eq!( + watcher.read(cx).notifications, + 2, + "both the projected write and the root write must reach the leaf" + ); + }); + } + + #[test] + fn observing_a_projection_fires_for_writes_from_either_side() { + struct Observer { + seen: Vec, + _subscription: Option, + } + + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + let name = render_projection(&mut cx, &person, |window, cx, person| { + crate::project!(window, cx, person, mut name) + }); + + let observer = cx.update(|cx| { + cx.new(|_| Observer { + seen: Vec::new(), + _subscription: None, + }) + }); + cx.update(|cx| { + observer.update(cx, |observer, cx| { + observer._subscription = Some(name.observe(cx, |this, name, cx| { + this.seen.push(name.read(cx).clone()); + })); + }) + }); + + cx.update(|cx| name.update(cx, |name| name.push_str(" Lovelace"))); + cx.update(|cx| { + person.update(cx, |person, cx| { + person.name = "Grace".to_string(); + cx.notify(); + }) + }); + + cx.update(|cx| { + assert_eq!( + observer.read(cx).seen, + vec!["Ada Lovelace".to_string(), "Grace".to_string()], + "both writes through the projection and writes straight to the \ + source must notify" + ); + }); + } + + #[test] + fn weak_projections_do_not_keep_the_source_alive() { + let mut cx = TestAppContext::single(); + let person = cx.update(|cx| { + cx.new(|_| Person { + name: "Ada".to_string(), + age: 36, + }) + }); + + let (window, frames) = hook_window(&mut cx, &person, |window, cx, person| { + crate::project!(window, cx, person, mut name) + }); + let name = take_output(&frames); + let weak_name = name.downgrade(); + let weak_read_only = name.read_only().downgrade(); + + { + let upgraded = weak_name.upgrade().expect("source is alive"); + cx.update(|cx| { + upgraded.update(cx, |name| name.push_str(" Lovelace")); + assert_eq!(upgraded.read(cx), "Ada Lovelace"); + }); + } + + // The write above invalidated the window, so let any re-render settle + // before dropping the strong handles it recorded. The window's element + // state holds the backing state entity as well, so the window has to go + // too before the weak handles can dangle. + cx.run_until_parked(); + window + .update(&mut cx, |_, window, _| window.remove_window()) + .unwrap(); + cx.run_until_parked(); + frames.borrow_mut().clear(); + drop(person); + drop(name); + cx.update(|_| {}); + + assert!(weak_name.upgrade().is_none()); + assert!(weak_read_only.upgrade().is_none()); + } +}