`. |
+| *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