Skip to content

gpui: Add ReadEntity and Projection entity handles - #60740

Closed
mikayla-maki wants to merge 8 commits into
mainfrom
fable/gpui-smart-pointer-types
Closed

gpui: Add ReadEntity and Projection entity handles#60740
mikayla-maki wants to merge 8 commits into
mainfrom
fable/gpui-smart-pointer-types

Conversation

@mikayla-maki

@mikayla-maki mikayla-maki commented Jul 10, 2026

Copy link
Copy Markdown
Member

Completes the entity handle lattice with read-only and lensed capability types:

whole entity lens
read + write Entity<T> ProjectionMut<P>
read only ReadEntity<T> Projection<P>

each with a weak twin (WeakReadEntity, WeakProjection, WeakProjectionMut), and conversions flowing write→read and whole→lens.

  • ReadEntity<T> restores the read/write distinction that shared-mutability entities erase: holders can read (and be re-rendered via access tracking) but cannot update or notify, making an entity's writer set auditable.
  • Projection<P>/ProjectionMut<P> are source-erased lens handles over part of an entity's state, so components can accept any P-shaped state (impl Into<ProjectionMut<String>>) without dictating how callers store it. P may be unsized (Projection<str>). Writes route through the source entity and always notify it.
  • Lens projections are constructed during render, via Window::use_projection/use_projection_mut (and use_keyed_* variants for loops), or by converting an Entity<P> with From. Each hook call site gets a memoized relay entity (like use_state) as the projection's identity, so sibling projections of different fields of one entity don't collide when used as view identities; the relay forwards source notifications so observation and caching keyed on the projection work. There is deliberately no way to construct a lens projection outside a render context — state that needs identity independent of any view should be a proper entity.
  • The project! macro derives both lenses of a mutable projection from a single field path: project!(window, cx, &person, address.city).
  • Lenses are non-capturing function pointers (capturing closures are rejected at compile time), stored type-erased next to a monomorphized trampoline, making projections allocation-free at construction beyond the relay memoized per call site.
  • Supporting APIs: AnyEntity::downcast_ref (backed by #[repr(transparent)] on Entity<T>) and EntityMap::read_any, so projection reads and writes avoid handle clones and refcount traffic.

Deliberate omissions, discussed and deferred: no equality-based notify dedupe (projecting from a notify-happy entity is a state-modeling smell — split the hot field into its own entity), and no observe/subscribe on read handles yet.

Release Notes:

  • N/A

@zed-community-bot zed-community-bot Bot added the staff Pull requests authored by a current member of Zed staff label Jul 10, 2026
@mikayla-maki mikayla-maki self-assigned this Jul 10, 2026
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jul 10, 2026
@mikayla-maki
mikayla-maki marked this pull request as draft July 11, 2026 23:01
mikayla-maki and others added 5 commits July 30, 2026 13:42
Completes the entity handle lattice with read-only and lensed
capability types:

- ReadEntity<T> / WeakReadEntity<T>: strong and weak read-only handles,
  restoring the read/write distinction that shared-mutability entities
  erase.
- Projection<P> / ProjectionMut<P> and weak variants: source-erased
  lens handles over part of an entity's state, so components can accept
  any P-shaped state without dictating how callers store it. Lenses are
  non-capturing function pointers, making projections allocation-free.
- AnyEntity::downcast_ref (backed by repr(transparent) on Entity) and
  EntityMap::read_any, so projection reads and writes avoid handle
  clones.
Lens projections now get their identity from a per-call-site relay
entity, memoized like use_state, so sibling projections of different
fields of one entity no longer collide when used as view identities.

- Entity::project/project_mut are crate-private; public construction is
  Window::use_projection{,_mut} and use_keyed_projection{,_mut} (or the
  identity From<Entity<P>> conversions).
- The relay entity forwards source notifications, so observation and
  caching keyed on the projection work.
- The project! macro derives both lenses of a mutable projection from a
  single field path.
…lenses

Removes all unsafe from projections: the type-erased lens fn pointers,
their transmuting trampolines, AnyEntity::downcast_ref, and the
repr(transparent) on Entity<T>. Projection handles now point at a state
entity that owns the source handle, lenses, and subscription, so escaped
handles retarget when a hook's source entity is swapped, and observers
are notified of the swap.
…t!(mut)

Replaces the private test constructors and the three per-test HookView
definitions with a single harness that runs a hook during render, so tests
build projections through the public use_projection path.

project! is now read-only by default, with a mut prefix for the writable
form.
@mikayla-maki
mikayla-maki force-pushed the fable/gpui-smart-pointer-types branch from 3a2ecee to 480c680 Compare July 30, 2026 21:00
The example now keeps its form in a single Profile entity and hands each
component a projection of the field it edits, built inline in the element
tree with project!. Input, TextArea and Editor take ProjectionMut<String>
instead of Entity<String>, so a component works the same whether its text
is a whole entity or one field of a larger struct.

Adds Projection::observe, which Editor needs to clamp its cursor when the
value is written from outside.
@mikayla-maki
mikayla-maki force-pushed the fable/gpui-smart-pointer-types branch from 1d7f04d to 6a93d1c Compare July 30, 2026 21:42
A projection's source can now be another projection, so a component handed
a ProjectionMut<Person> can project its fields without knowing whether the
person is an entity or a field of something larger. Composition walks lenses
rather than copying: writes recurse down to the entity that owns the value,
notifications climb back up.

Also fixes a pre-existing feedback loop in the example. A view's identity is
the notify target for state allocated inside it, so an Input identified by
the value it allocates an editor over routed that editor's notifications
back into the value it observes and spun forever. This reproduced on the
example before any projection work, with a plain Entity<String>.
@SomeoneToIgnore

SomeoneToIgnore commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

To keep the PR queue cleaner, will close this PR for now, as this is a draft still with no changes for over 3 weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement staff Pull requests authored by a current member of Zed staff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants