Add focus-driven blink and render log

Mikayla Maki created

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

Change summary

crates/gpui/examples/view_example/example_editor.rs      | 40 +++
crates/gpui/examples/view_example/example_editor_info.rs |  8 
crates/gpui/examples/view_example/example_input.rs       | 15 +
crates/gpui/examples/view_example/example_render_log.rs  | 98 ++++++++++
crates/gpui/examples/view_example/example_tests.rs       | 21 +
crates/gpui/examples/view_example/example_text_area.rs   |  8 
crates/gpui/examples/view_example/view_example_main.rs   | 39 ++-
7 files changed, 189 insertions(+), 40 deletions(-)

Detailed changes

crates/gpui/examples/view_example/example_editor.rs 🔗

@@ -8,11 +8,12 @@ use std::time::Duration;
 
 use gpui::{
     App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
-    LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Task, TextRun, UTF16Selection, Window,
-    fill, hsla, point, prelude::*, px, relative, size,
+    LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task, TextRun,
+    UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
 };
 use unicode_segmentation::*;
 
+use crate::example_render_log::RenderLog;
 use crate::{Backspace, Delete, End, Home, Left, Right};
 
 pub struct ExampleEditor {
@@ -20,22 +21,44 @@ pub struct ExampleEditor {
     pub content: String,
     pub cursor: usize,
     pub cursor_visible: bool,
+    pub render_log: Option<Entity<RenderLog>>,
     _blink_task: Task<()>,
+    _subscriptions: Vec<Subscription>,
 }
 
 impl ExampleEditor {
-    pub fn new(cx: &mut Context<Self>) -> Self {
-        let blink_task = Self::spawn_blink_task(cx);
+    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let focus_handle = cx.focus_handle();
+
+        let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
+            this.start_blink(cx);
+        });
+        let blur_sub = cx.on_blur(&focus_handle, window, |this, _window, cx| {
+            this.stop_blink(cx);
+        });
 
         Self {
-            focus_handle: cx.focus_handle(),
+            focus_handle,
             content: String::new(),
             cursor: 0,
-            cursor_visible: true,
-            _blink_task: blink_task,
+            cursor_visible: false,
+            render_log: None,
+            _blink_task: Task::ready(()),
+            _subscriptions: vec![focus_sub, blur_sub],
         }
     }
 
+    pub fn start_blink(&mut self, cx: &mut Context<Self>) {
+        self.cursor_visible = true;
+        self._blink_task = Self::spawn_blink_task(cx);
+    }
+
+    pub fn stop_blink(&mut self, cx: &mut Context<Self>) {
+        self.cursor_visible = false;
+        self._blink_task = Task::ready(());
+        cx.notify();
+    }
+
     fn spawn_blink_task(cx: &mut Context<Self>) -> Task<()> {
         cx.spawn(async move |this, cx| {
             loop {
@@ -450,6 +473,9 @@ impl gpui::EntityView for ExampleEditor {
         _window: &mut Window,
         cx: &mut Context<ExampleEditor>,
     ) -> impl IntoElement {
+        if let Some(render_log) = self.render_log.clone() {
+            render_log.update(cx, |log, _cx| log.log("ExampleEditor"));
+        }
         ExampleEditorText::new(cx.entity().clone())
     }
 }

crates/gpui/examples/view_example/example_editor_info.rs 🔗

@@ -7,15 +7,17 @@
 use gpui::{App, Entity, IntoViewElement, Window, div, hsla, prelude::*, px};
 
 use crate::example_editor::ExampleEditor;
+use crate::example_render_log::RenderLog;
 
 #[derive(Hash, IntoViewElement)]
 pub struct EditorInfo {
     editor: Entity<ExampleEditor>,
+    render_log: Entity<RenderLog>,
 }
 
 impl EditorInfo {
-    pub fn new(editor: Entity<ExampleEditor>) -> Self {
-        Self { editor }
+    pub fn new(editor: Entity<ExampleEditor>, render_log: Entity<RenderLog>) -> Self {
+        Self { editor, render_log }
     }
 }
 
@@ -27,6 +29,8 @@ impl gpui::View for EditorInfo {
     }
 
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        self.render_log.update(cx, |log, _cx| log.log("EditorInfo"));
+
         let editor = self.editor.read(cx);
         let char_count = editor.content.len();
         let cursor = editor.cursor;

crates/gpui/examples/view_example/example_input.rs 🔗

@@ -14,6 +14,7 @@ use gpui::{
 };
 
 use crate::example_editor::ExampleEditor;
+use crate::example_render_log::RenderLog;
 use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
 pub struct ExampleInputState {
@@ -25,8 +26,11 @@ pub struct ExampleInputState {
 }
 
 impl ExampleInputState {
-    pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
-        let editor = cx.new(|cx| ExampleEditor::new(cx));
+    pub fn new(render_log: Entity<RenderLog>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+        let editor = cx.new(|cx| ExampleEditor::new(window, cx));
+        editor.update(cx, |e, _cx| {
+            e.render_log = Some(render_log);
+        });
         let focus_handle = editor.read(cx).focus_handle.clone();
 
         let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
@@ -51,14 +55,16 @@ impl ExampleInputState {
 #[derive(Hash, IntoViewElement)]
 pub struct ExampleInput {
     state: Entity<ExampleInputState>,
+    render_log: Entity<RenderLog>,
     width: Option<Pixels>,
     color: Option<Hsla>,
 }
 
 impl ExampleInput {
-    pub fn new(state: Entity<ExampleInputState>) -> Self {
+    pub fn new(state: Entity<ExampleInputState>, render_log: Entity<RenderLog>) -> Self {
         Self {
             state,
+            render_log,
             width: None,
             color: None,
         }
@@ -92,6 +98,9 @@ impl gpui::View for ExampleInput {
     }
 
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        self.render_log
+            .update(cx, |log, _cx| log.log("ExampleInput"));
+
         let input_state = self.state.read(cx);
         let count = input_state.flash_count;
         let editor = input_state.editor.clone();

crates/gpui/examples/view_example/example_render_log.rs 🔗

@@ -0,0 +1,98 @@
+//! `RenderLog` — a diagnostic panel that records which components re-render
+//! and when, letting you observe GPUI's caching behaviour in real time.
+
+use std::time::Instant;
+
+use gpui::{App, Context, Entity, IntoViewElement, Window, div, hsla, prelude::*, px};
+
+// ---------------------------------------------------------------------------
+// RenderLog entity
+// ---------------------------------------------------------------------------
+
+pub struct RenderLog {
+    entries: Vec<RenderLogEntry>,
+    start_time: Instant,
+}
+
+struct RenderLogEntry {
+    component: &'static str,
+    timestamp: Instant,
+}
+
+impl RenderLog {
+    pub fn new(_cx: &mut Context<Self>) -> Self {
+        Self {
+            entries: Vec::new(),
+            start_time: Instant::now(),
+        }
+    }
+
+    /// Record that `component` rendered. Does **not** call `cx.notify()` — the
+    /// panel updates passively the next time its parent re-renders, which avoids
+    /// an infinite invalidation loop.
+    pub fn log(&mut self, component: &'static str) {
+        self.entries.push(RenderLogEntry {
+            component,
+            timestamp: Instant::now(),
+        });
+        if self.entries.len() > 50 {
+            self.entries.drain(0..self.entries.len() - 50);
+        }
+    }
+}
+
+// ---------------------------------------------------------------------------
+// RenderLogPanel — stateless ComponentView that displays the log
+// ---------------------------------------------------------------------------
+
+#[derive(Hash, IntoViewElement)]
+pub struct RenderLogPanel {
+    log: Entity<RenderLog>,
+}
+
+impl RenderLogPanel {
+    pub fn new(log: Entity<RenderLog>) -> Self {
+        Self { log }
+    }
+}
+
+impl gpui::ComponentView for RenderLogPanel {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        let log = self.log.read(cx);
+        let start = log.start_time;
+
+        div()
+            .flex()
+            .flex_col()
+            .gap(px(1.))
+            .p(px(8.))
+            .bg(hsla(0., 0., 0.12, 1.))
+            .rounded(px(4.))
+            .max_h(px(180.))
+            .overflow_hidden()
+            .child(
+                div()
+                    .text_xs()
+                    .text_color(hsla(0., 0., 0.55, 1.))
+                    .mb(px(4.))
+                    .child("Render log (most recent 20)"),
+            )
+            .children(
+                log.entries
+                    .iter()
+                    .rev()
+                    .take(20)
+                    .collect::<Vec<_>>()
+                    .into_iter()
+                    .rev()
+                    .map(|entry| {
+                        let elapsed = entry.timestamp.duration_since(start);
+                        let secs = elapsed.as_secs_f64();
+                        div()
+                            .text_xs()
+                            .text_color(hsla(120. / 360., 0.7, 0.65, 1.))
+                            .child(format!("{:<20} +{:.1}s", entry.component, secs))
+                    }),
+            )
+    }
+}

crates/gpui/examples/view_example/example_tests.rs 🔗

@@ -14,26 +14,29 @@ mod tests {
 
     use crate::example_editor::ExampleEditor;
     use crate::example_input::ExampleInput;
+    use crate::example_render_log::RenderLog;
     use crate::example_text_area::ExampleTextArea;
     use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
     struct InputWrapper {
         editor: Entity<ExampleEditor>,
+        render_log: Entity<RenderLog>,
     }
 
     impl Render for InputWrapper {
         fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-            ExampleInput::new(self.editor.clone())
+            ExampleInput::new(self.editor.clone(), self.render_log.clone())
         }
     }
 
     struct TextAreaWrapper {
         editor: Entity<ExampleEditor>,
+        render_log: Entity<RenderLog>,
     }
 
     impl Render for TextAreaWrapper {
         fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-            ExampleTextArea::new(self.editor.clone(), 5)
+            ExampleTextArea::new(self.editor.clone(), self.render_log.clone(), 5)
         }
     }
 
@@ -56,9 +59,10 @@ mod tests {
     ) -> (Entity<ExampleEditor>, &mut gpui::VisualTestContext) {
         bind_keys(cx);
 
-        let (wrapper, cx) = cx.add_window_view(|_window, cx| {
-            let editor = cx.new(|cx| ExampleEditor::new(cx));
-            InputWrapper { editor }
+        let (wrapper, cx) = cx.add_window_view(|window, cx| {
+            let editor = cx.new(|cx| ExampleEditor::new(window, cx));
+            let render_log = cx.new(|cx| RenderLog::new(cx));
+            InputWrapper { editor, render_log }
         });
 
         let editor = cx.read_entity(&wrapper, |wrapper, _cx| wrapper.editor.clone());
@@ -76,9 +80,10 @@ mod tests {
     ) -> (Entity<ExampleEditor>, &mut gpui::VisualTestContext) {
         bind_keys(cx);
 
-        let (wrapper, cx) = cx.add_window_view(|_window, cx| {
-            let editor = cx.new(|cx| ExampleEditor::new(cx));
-            TextAreaWrapper { editor }
+        let (wrapper, cx) = cx.add_window_view(|window, cx| {
+            let editor = cx.new(|cx| ExampleEditor::new(window, cx));
+            let render_log = cx.new(|cx| RenderLog::new(cx));
+            TextAreaWrapper { editor, render_log }
         });
 
         let editor = cx.read_entity(&wrapper, |wrapper, _cx| wrapper.editor.clone());

crates/gpui/examples/view_example/example_text_area.rs 🔗

@@ -10,19 +10,22 @@ use gpui::{
 };
 
 use crate::example_editor::ExampleEditor;
+use crate::example_render_log::RenderLog;
 use crate::{Backspace, Delete, End, Enter, Home, Left, Right};
 
 #[derive(Hash, IntoViewElement)]
 pub struct ExampleTextArea {
     editor: Entity<ExampleEditor>,
+    render_log: Entity<RenderLog>,
     rows: usize,
     color: Option<Hsla>,
 }
 
 impl ExampleTextArea {
-    pub fn new(editor: Entity<ExampleEditor>, rows: usize) -> Self {
+    pub fn new(editor: Entity<ExampleEditor>, render_log: Entity<RenderLog>, rows: usize) -> Self {
         Self {
             editor,
+            render_log,
             rows,
             color: None,
         }
@@ -36,6 +39,9 @@ impl ExampleTextArea {
 
 impl gpui::ComponentView for ExampleTextArea {
     fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        self.render_log
+            .update(cx, |log, _cx| log.log("ExampleTextArea"));
+
         let focus_handle = self.editor.read(cx).focus_handle.clone();
         let is_focused = focus_handle.is_focused(window);
         let text_color = self.color.unwrap_or(hsla(0., 0., 0.1, 1.));

crates/gpui/examples/view_example/view_example_main.rs 🔗

@@ -30,6 +30,7 @@
 mod example_editor;
 mod example_editor_info;
 mod example_input;
+mod example_render_log;
 mod example_text_area;
 
 #[cfg(test)]
@@ -44,6 +45,7 @@ use gpui_platform::application;
 use example_editor::ExampleEditor;
 use example_editor_info::EditorInfo;
 use example_input::{ExampleInput, ExampleInputState};
+use example_render_log::{RenderLog, RenderLogPanel};
 use example_text_area::ExampleTextArea;
 
 actions!(
@@ -71,9 +73,18 @@ impl ViewExample {
 
 impl Render for ViewExample {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let input_state = window.use_state(cx, |window, cx| ExampleInputState::new(window, cx));
+        let render_log = window.use_state(cx, |_window, cx| RenderLog::new(cx));
+        let input_state = window.use_state(cx, {
+            let render_log = render_log.clone();
+            move |window, cx| ExampleInputState::new(render_log, window, cx)
+        });
         let input_editor = input_state.read(cx).editor.clone();
-        let textarea_editor = window.use_state(cx, |_window, cx| ExampleEditor::new(cx));
+        let textarea_editor = window.use_state(cx, |window, cx| ExampleEditor::new(window, cx));
+        textarea_editor.update(cx, |e, _cx| {
+            if e.render_log.is_none() {
+                e.render_log = Some(render_log.clone());
+            }
+        });
         let input_color = self.input_color;
         let textarea_color = self.textarea_color;
 
@@ -96,11 +107,11 @@ impl Render for ViewExample {
                             .child("Single-line input (View with own state + cached editor)"),
                     )
                     .child(
-                        ExampleInput::new(input_state)
+                        ExampleInput::new(input_state, render_log.clone())
                             .width(px(320.))
                             .color(input_color),
                     )
-                    .child(EditorInfo::new(input_editor)),
+                    .child(EditorInfo::new(input_editor, render_log.clone())),
             )
             .child(
                 div()
@@ -110,22 +121,12 @@ impl Render for ViewExample {
                     .child(div().text_sm().text_color(hsla(0., 0., 0.3, 1.)).child(
                         "Multi-line text area (TextArea — same entity type, different View)",
                     ))
-                    .child(ExampleTextArea::new(textarea_editor, 5).color(textarea_color)),
-            )
-            .child(
-                div()
-                    .flex()
-                    .flex_col()
-                    .gap(px(2.))
-                    .mt(px(12.))
-                    .text_xs()
-                    .text_color(hsla(0., 0., 0.5, 1.))
-                    .child("• ExampleInput: View with own state — caches independently")
-                    .child("• EditorInfo: View on same editor — zero-wiring, auto-cached")
-                    .child("• ExampleTextArea: ComponentView — stateless wrapper")
-                    .child("• Press Enter in input to flash border (EditorInfo stays cached)")
-                    .child("• Type to see both input and info update reactively"),
+                    .child(
+                        ExampleTextArea::new(textarea_editor, render_log.clone(), 5)
+                            .color(textarea_color),
+                    ),
             )
+            .child(RenderLogPanel::new(render_log))
     }
 }