Detailed changes
@@ -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())
}
}
@@ -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;
@@ -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();
@@ -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))
+ }),
+ )
+ }
+}
@@ -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());
@@ -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.));
@@ -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))
}
}