Skip to content

gpui: Introduce the View trait - #51030

Closed
mikayla-maki wants to merge 12 commits into
mainfrom
gpui-components-and-views
Closed

gpui: Introduce the View trait#51030
mikayla-maki wants to merge 12 commits into
mainfrom
gpui-components-and-views

Conversation

@mikayla-maki

Copy link
Copy Markdown
Member

Introduces a new player in GPUI's narrative: The View, a tuple of (Entity, Component), that makes the Component approach work in GPUI.

Release Notes:

  • N/A

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Mar 7, 2026
@zed-community-bot zed-community-bot Bot added the staff Pull requests authored by a current member of Zed staff label Mar 7, 2026
@mikayla-maki mikayla-maki changed the title Gpui components and views gpui: Introduce the View trait Mar 7, 2026
Comment thread crates/gpui/src/view.rs Outdated
Comment thread crates/gpui/src/view.rs Outdated
…model

Introduce View trait with Option<Entity> for stateful/stateless branching,
ComponentView trait with blanket View impl for RenderOnce-style components,
and ViewElement that handles both cached (reactive boundary) and uncached
(type-name isolation) paths.

Rename text_views example to view_example with ExampleEditor/Input/TextArea.
- EntityView: View-based replacement for Render on entity state types.
  Entity<T: EntityView> gets a blanket View impl with reactive boundary.
- Use ViewElement::new(entity) at call sites for IntoElement.
- Add #[inline] on ComponentView and EntityView bridge methods.
- Tighten all public docs in view.rs to be short and consistent.
- Update ExampleEditor to use EntityView, remove ExampleEditorView wrapper.
- ExampleEditorText reads text_color from inherited text style.
…Area as ComponentView

ExampleInput is now a proper View backed by ExampleInputState, which:
- Creates and owns its editor entity internally (cx.new in state constructor)
- Tracks focus via on_focus/on_blur listeners on its own state entity
- Never reads the editor entity during render — only reads its own state
- Gets an independent caching boundary from both parent and child editor

ExampleTextArea is now a ComponentView (stateless wrapper) where the inner
EntityView does the caching. This demonstrates the cheaper pattern for
components that don't need their own reactive boundary.

The parent (ViewExample) allocates ExampleInputState via use_state, which
internally chains to cx.new() for the editor. ExampleInput::new() is a
pure constructor taking just the state handle.

Design analysis documented in plan.md:
- use_state observer is necessary (non-view entities aren't in dispatch tree)
- View vs ComponentView vs EntityView taxonomy with clear use cases
- Entity reactivity taxonomy (view, component-local, shared-universal, shared-selective)
- V2 use_global API concept for theme/settings auto-reactivity
EditorInfo is a View that reads from the same ExampleEditor entity as
ExampleInput, demonstrating two completely different renderings of one
entity with automatic caching and no manual wiring:

- Returns the editor from entity() — that's the entire reactive hookup
- Reads editor.content, cursor, focus_handle in render()
- Caches automatically: typing updates it, but pressing Enter (flash
  border on input) leaves it cached since the editor didn't change
- 46 lines total, no observers, no subscriptions, no cx.notify()

The parent shares the editor handle by reading it from ExampleInputState:
  let input_editor = input_state.read(cx).editor.clone();
  EditorInfo::new(input_editor)
Blink fix:
- ExampleEditor no longer starts a blink task unconditionally
- Registers on_focus/on_blur listeners in constructor (now takes Window)
- start_blink() spawns the timer, stop_blink() drops it
- Unfocused editors produce zero re-renders

Render log:
- New RenderLog entity + RenderLogPanel ComponentView
- Each component logs its render() call: ExampleInput, EditorInfo,
  ExampleTextArea, and ExampleEditor
- Panel displays last 20 entries with timestamps, dark terminal style
- No cx.notify() in log() — avoids infinite invalidation loop;
  panel updates passively when parent re-renders
- Replaces the static help text at the bottom of the example
…ive frames

Rewrote RenderLog to group render() calls by frame:
- begin_frame() called at start of root render, finalizes previous frame
- log() records component name in current frame
- Consecutive frames with identical component sets collapse into one
  entry with a ×N counter (e.g. animation re-rendering Input+Editor
  30 times shows as one line with ×30)
- Display shows component names, repeat count, and timestamp

Added 6 unit tests for the frame grouping logic:
- Groups by frame, consecutive collapse, different frames don't collapse,
  collapse resumes after interruption, empty frames ignored, dedup
  within frame

Note: tests are plain #[test] (not gpui::test) since RenderLog logic
is pure data manipulation. The pre-existing example_tests.rs compilation
issue (TestAppContext not available in examples) prevents running them
via cargo test currently.
- All 6 render_log tests pass as plain #[test] (pure data logic)
- Fixed example_tests to use ExampleInputState (matching new API)
- Fixed blink test for focus-driven blink (test via typing, not init)
- Fixed test command in doc comment
- All 12 tests pass with: cargo test --example view_example -p gpui --features test-support
@mikayla-maki
mikayla-maki force-pushed the gpui-components-and-views branch from 8c21970 to b0facda Compare March 15, 2026 23:32
@bowenxuuu

Copy link
Copy Markdown
Contributor

Any progress?

@SomeoneToIgnore

Copy link
Copy Markdown
Contributor

Autoclosing all draft PRs with no activity for 2 weeks or over, this one also has the CI failing:

image

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.

4 participants