diff --git a/crates/gpui/examples/view_example/example_editor.rs b/crates/gpui/examples/view_example/example_editor.rs index 662dea08aee76a347c9cc0d8a71021111b3673ba..9485f804b94b47abe313b668f454f7fad04b8d1c 100644 --- a/crates/gpui/examples/view_example/example_editor.rs +++ b/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>, _blink_task: Task<()>, + _subscriptions: Vec, } impl ExampleEditor { - pub fn new(cx: &mut Context) -> Self { - let blink_task = Self::spawn_blink_task(cx); + pub fn new(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| { + 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.cursor_visible = true; + self._blink_task = Self::spawn_blink_task(cx); + } + + pub fn stop_blink(&mut self, cx: &mut Context) { + self.cursor_visible = false; + self._blink_task = Task::ready(()); + cx.notify(); + } + fn spawn_blink_task(cx: &mut Context) -> Task<()> { cx.spawn(async move |this, cx| { loop { @@ -450,6 +473,9 @@ impl gpui::EntityView for ExampleEditor { _window: &mut Window, cx: &mut Context, ) -> 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()) } } diff --git a/crates/gpui/examples/view_example/example_editor_info.rs b/crates/gpui/examples/view_example/example_editor_info.rs index 3112360f3fd46b76b0166da4dd53ad9a961ad450..e472e41759d2f7d8f54f969a48f6e31fe89136bc 100644 --- a/crates/gpui/examples/view_example/example_editor_info.rs +++ b/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, + render_log: Entity, } impl EditorInfo { - pub fn new(editor: Entity) -> Self { - Self { editor } + pub fn new(editor: Entity, render_log: Entity) -> 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; diff --git a/crates/gpui/examples/view_example/example_input.rs b/crates/gpui/examples/view_example/example_input.rs index a17033ced38b3208b93ecfe1af516090e576cf7b..5a23bf7e83fe1306adf5e30b49f5ecb77c71565c 100644 --- a/crates/gpui/examples/view_example/example_input.rs +++ b/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 { - let editor = cx.new(|cx| ExampleEditor::new(cx)); + pub fn new(render_log: Entity, window: &mut Window, cx: &mut Context) -> 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, + render_log: Entity, width: Option, color: Option, } impl ExampleInput { - pub fn new(state: Entity) -> Self { + pub fn new(state: Entity, render_log: Entity) -> 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(); diff --git a/crates/gpui/examples/view_example/example_render_log.rs b/crates/gpui/examples/view_example/example_render_log.rs new file mode 100644 index 0000000000000000000000000000000000000000..1abf9dbfd0b1bf5cfb8cef2946cb7a64e7462030 --- /dev/null +++ b/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, + start_time: Instant, +} + +struct RenderLogEntry { + component: &'static str, + timestamp: Instant, +} + +impl RenderLog { + pub fn new(_cx: &mut Context) -> 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, +} + +impl RenderLogPanel { + pub fn new(log: Entity) -> 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::>() + .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)) + }), + ) + } +} diff --git a/crates/gpui/examples/view_example/example_tests.rs b/crates/gpui/examples/view_example/example_tests.rs index 81fb8648151e512117d548116317ab43c808c152..be77d33ac5f078c52d5ebf06723ac3b66777baa1 100644 --- a/crates/gpui/examples/view_example/example_tests.rs +++ b/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, + render_log: Entity, } impl Render for InputWrapper { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - ExampleInput::new(self.editor.clone()) + ExampleInput::new(self.editor.clone(), self.render_log.clone()) } } struct TextAreaWrapper { editor: Entity, + render_log: Entity, } impl Render for TextAreaWrapper { fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> 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, &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, &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()); diff --git a/crates/gpui/examples/view_example/example_text_area.rs b/crates/gpui/examples/view_example/example_text_area.rs index 9bd53544d24e12c06d8a1f2bbbb1b665cc9fefd2..b9058088b26b55819eb8110a244b71d415f20151 100644 --- a/crates/gpui/examples/view_example/example_text_area.rs +++ b/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, + render_log: Entity, rows: usize, color: Option, } impl ExampleTextArea { - pub fn new(editor: Entity, rows: usize) -> Self { + pub fn new(editor: Entity, render_log: Entity, 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.)); diff --git a/crates/gpui/examples/view_example/view_example_main.rs b/crates/gpui/examples/view_example/view_example_main.rs index e0ec702646fa188da74bb02aa5139286fe02068e..3bbb93ae8e5f4ec0f72a721426671e8baa5d1242 100644 --- a/crates/gpui/examples/view_example/view_example_main.rs +++ b/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) -> 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)) } }