perf(ui-gpui): ProjectBoxFullElement wraps entire card, 2.1% CPU

Single custom Element for header+tabs (paint_chrome) + content AnyElement.
Only 1 div remains (content wrapper). Down from 90% → 2.1% over 11 iterations.
This commit is contained in:
Hibryda 2026-03-20 00:32:11 +01:00
parent 5a7a5ad621
commit 3bbaefa9a2
2 changed files with 224 additions and 146 deletions

View file

@ -9,10 +9,9 @@
use gpui::*;
use crate::CachedView;
use crate::components::project_box_element::ProjectBoxHeaderElement;
use crate::components::project_box_element::ProjectBoxFullElement;
use crate::state::{AgentStatus, Project, ProjectTab};
use crate::theme;
// blink_state used via fully-qualified path in init_subviews + render
// ── Accent Colors by Index ──────────────────────────────────────────
@ -55,7 +54,7 @@ fn tab_button(label: &'static str, active: bool, accent: Rgba) -> Div {
.text_color(if active { accent } else { theme::SUBTEXT0 })
.cursor_pointer();
if active { tab = tab.border_b_2().border_color(accent); }
tab.child(label) // &'static str → no allocation
tab.child(label)
}
// ── ProjectBox View ─────────────────────────────────────────────────
@ -68,9 +67,6 @@ pub struct ProjectBox {
// Cached strings to avoid allocation on every render
id_project: SharedString,
id_model: SharedString,
id_resize: SharedString,
id_docs: SharedString,
id_files: SharedString,
cached_name: SharedString,
cached_cwd: SharedString,
}
@ -81,9 +77,6 @@ impl ProjectBox {
Self {
id_project: SharedString::from(format!("project-{id}")),
id_model: SharedString::from(format!("model-{id}")),
id_resize: SharedString::from(format!("resize-{id}")),
id_docs: SharedString::from(format!("docs-{id}")),
id_files: SharedString::from(format!("files-{id}")),
cached_name: SharedString::from(project.name.clone()),
cached_cwd: SharedString::from(project.cwd.clone()),
project,
@ -95,16 +88,12 @@ impl ProjectBox {
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
// Initialize sub-views after entity registration
// SharedBlink: Arc<AtomicBool> toggled by background timer.
// Timer calls cx.notify() on THIS ProjectBox directly — no intermediate entities.
// mark_view_dirty walks: ProjectBox → Workspace (2 levels only).
let should_pulse = matches!(self.project.agent.status, AgentStatus::Running)
&& self.project.accent_index == 0;
if should_pulse {
let blink = crate::components::blink_state::SharedBlink::new();
// Get our own entity handle to pass to the timer
let self_entity = cx.entity().downgrade();
let visible = blink.visible.clone();
cx.spawn(async move |_: WeakEntity<Self>, cx: &mut AsyncApp| {
@ -144,7 +133,7 @@ impl Render for ProjectBox {
ProjectTab::Files => 2,
};
// Content area — only part that uses divs (cached children for Model tab)
// Content area — single div with cached children for the Model tab.
let mut content = div()
.id(self.id_model.clone())
.flex_1()
@ -171,31 +160,17 @@ impl Render for ProjectBox {
.child("src/").child(" main.rs").child(" lib.rs").child("Cargo.toml"),
};
// Root: 1 outer div + custom header/tab Element + content div
// Total divs: 2 (root + content) — down from 12
div()
.id(self.id_project.clone())
.flex_1()
.min_w(px(400.0))
.min_h(px(300.0))
.flex()
.flex_col()
.bg(theme::BASE)
.rounded(px(8.0))
.border_1()
.border_color(theme::SURFACE0)
.overflow_hidden()
// Header + Tab Bar: single custom Element (0 divs, ~10 GPU primitives)
.child(ProjectBoxHeaderElement {
id: self.id_project.clone().into(),
name: self.cached_name.clone(),
cwd: self.cached_cwd.clone(),
accent,
status: self.project.agent.status,
blink_visible: self.shared_blink.as_ref().map(|b| b.visible.clone()),
active_tab: tab_idx,
})
// Content area (cached child views for Model tab)
.child(content)
// Return a single custom Element that owns the entire card.
// Eliminates the outer root div — down from 2 divs to 1 (only content div remains).
ProjectBoxFullElement {
id: self.id_project.clone().into(),
name: self.cached_name.clone(),
cwd: self.cached_cwd.clone(),
accent,
status: self.project.agent.status,
blink_visible: self.shared_blink.as_ref().map(|b| b.visible.clone()),
active_tab: tab_idx,
content: content.into_any_element(),
}
}
}

View file

@ -1,5 +1,7 @@
//! ProjectBoxHeaderElement — custom Element painting header + tab bar directly.
//! Zero div overhead for these regions. Content area delegates to child AnyView.
//! ProjectBox custom Elements:
//! - `ProjectBoxHeaderElement` — paints header + tab bar directly (zero div overhead)
//! - `ProjectBoxFullElement` — wraps header paint + content AnyElement as a single
//! custom Element, eliminating the outer root div from ProjectBox::render()
use gpui::*;
use std::sync::atomic::Ordering;
@ -9,9 +11,124 @@ use crate::theme;
pub const HEADER_HEIGHT: f32 = 36.0;
pub const TAB_BAR_HEIGHT: f32 = 28.0;
pub const CHROME_HEIGHT: f32 = HEADER_HEIGHT + TAB_BAR_HEIGHT;
const TAB_LABELS: [&str; 3] = ["Model", "Docs", "Files"];
// ── Shared paint helper ───────────────────────────────────────────────────────
/// Paint header background + tab bar into `bounds`. Used by both Elements.
fn paint_chrome(
bounds: Bounds<Pixels>,
name: &SharedString,
cwd: &SharedString,
accent: Rgba,
status: AgentStatus,
blink_visible: &Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
active_tab: usize,
window: &mut Window,
cx: &mut App,
) {
let o = bounds.origin;
let w = bounds.size.width;
let hh = px(HEADER_HEIGHT);
let tbh = px(TAB_BAR_HEIGHT);
// Header background (rounded top corners)
window.paint_quad(PaintQuad {
bounds: Bounds { origin: o, size: size(w, hh) },
corner_radii: Corners {
top_left: px(6.0), top_right: px(6.0),
bottom_left: px(0.0), bottom_right: px(0.0),
},
background: theme::MANTLE.into(),
border_widths: Edges::default(),
border_color: transparent_black(),
border_style: BorderStyle::default(),
});
// Header bottom border
window.paint_quad(fill(
Bounds { origin: point(o.x, o.y + hh - px(1.0)), size: size(w, px(1.0)) },
theme::SURFACE0,
));
// Accent stripe
let stripe_x = o.x + px(12.0);
window.paint_quad(
fill(Bounds { origin: point(stripe_x, o.y + (hh - px(20.0)) * 0.5),
size: size(px(3.0), px(20.0)) }, accent)
.corner_radii(px(2.0)),
);
// Status dot
let dot_color = match status {
AgentStatus::Running => if blink_visible.as_ref()
.map(|b| b.load(Ordering::Relaxed)).unwrap_or(true)
{ theme::GREEN } else { theme::SURFACE1 },
AgentStatus::Idle => theme::OVERLAY0,
AgentStatus::Done => theme::BLUE,
AgentStatus::Error => theme::RED,
};
let dot_x = stripe_x + px(11.0);
window.paint_quad(
fill(Bounds { origin: point(dot_x, o.y + (hh - px(8.0)) * 0.5),
size: size(px(8.0), px(8.0)) }, dot_color)
.corner_radii(px(4.0)),
);
// Name + CWD text
let ts = window.text_system().clone();
if !name.contains('\n') {
let run = TextRun { len: name.len(), font: font(".SystemUIFont"),
color: theme::TEXT.into(), background_color: None,
underline: None, strikethrough: None };
let shaped = ts.shape_line(name.clone(), px(13.0), &[run], None);
let _ = shaped.paint(point(dot_x + px(12.0), o.y + (hh - px(13.0)) * 0.5), hh, window, cx);
}
if !cwd.contains('\n') {
let run = TextRun { len: cwd.len(), font: font(".SystemUIFont"),
color: theme::OVERLAY0.into(), background_color: None,
underline: None, strikethrough: None };
let shaped = ts.shape_line(cwd.clone(), px(10.0), &[run], None);
let cwd_x = o.x + w - shaped.width - px(12.0);
let _ = shaped.paint(point(cwd_x, o.y + (hh - px(10.0)) * 0.5), hh, window, cx);
}
// Tab bar
let tab_y = o.y + hh;
window.paint_quad(fill(
Bounds { origin: point(o.x, tab_y), size: size(w, tbh) },
theme::MANTLE,
));
window.paint_quad(fill(
Bounds { origin: point(o.x, tab_y + tbh - px(1.0)), size: size(w, px(1.0)) },
theme::SURFACE0,
));
let mut tab_x = o.x + px(8.0);
for (i, label) in TAB_LABELS.iter().enumerate() {
let active = i == active_tab;
let color: Hsla = if active { accent.into() } else { theme::SUBTEXT0.into() };
let run = TextRun { len: label.len(), font: font(".SystemUIFont"),
color, background_color: None,
underline: None, strikethrough: None };
let shaped = ts.shape_line(SharedString::from(*label), px(11.0), &[run], None);
let label_y = tab_y + (tbh - px(11.0)) * 0.5;
let _ = shaped.paint(point(tab_x, label_y), tbh, window, cx);
if active {
window.paint_quad(fill(
Bounds { origin: point(tab_x, tab_y + tbh - px(3.0)),
size: size(shaped.width, px(2.0)) },
accent,
));
}
tab_x = tab_x + shaped.width + px(12.0);
}
}
// ── ProjectBoxHeaderElement ───────────────────────────────────────────────────
/// Standalone header + tab bar element. Used when the outer container is still a div.
/// Kept for compatibility; ProjectBoxFullElement supersedes it.
pub struct ProjectBoxHeaderElement {
pub id: ElementId,
pub name: SharedString,
@ -19,7 +136,7 @@ pub struct ProjectBoxHeaderElement {
pub accent: Rgba,
pub status: AgentStatus,
pub blink_visible: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
pub active_tab: usize, // 0=Model, 1=Docs, 2=Files
pub active_tab: usize,
}
impl IntoElement for ProjectBoxHeaderElement {
@ -38,11 +155,11 @@ impl Element for ProjectBoxHeaderElement {
&mut self, _id: Option<&GlobalElementId>, _iid: Option<&InspectorElementId>,
window: &mut Window, cx: &mut App,
) -> (LayoutId, ()) {
let total_h = HEADER_HEIGHT + TAB_BAR_HEIGHT;
let style = Style {
size: Size {
width: Length::Definite(relative(1.0)),
height: Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(total_h)))),
height: Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(
px(CHROME_HEIGHT)))),
},
flex_shrink: 0.0,
..Style::default()
@ -60,106 +177,92 @@ impl Element for ProjectBoxHeaderElement {
bounds: Bounds<Pixels>, _rl: &mut (), _pp: &mut (),
window: &mut Window, cx: &mut App,
) {
let o = bounds.origin;
let w = bounds.size.width;
let hh = px(HEADER_HEIGHT);
let tbh = px(TAB_BAR_HEIGHT);
// ── Header Background ──
window.paint_quad(PaintQuad {
bounds: Bounds { origin: o, size: size(w, hh) },
corner_radii: Corners { top_left: px(6.0), top_right: px(6.0),
bottom_left: px(0.0), bottom_right: px(0.0) },
background: theme::MANTLE.into(),
border_widths: Edges::default(),
border_color: transparent_black(),
border_style: BorderStyle::default(),
});
// Header bottom border
window.paint_quad(fill(
Bounds { origin: point(o.x, o.y + hh - px(1.0)), size: size(w, px(1.0)) },
theme::SURFACE0,
));
// Accent stripe
let stripe_x = o.x + px(12.0);
window.paint_quad(
fill(Bounds { origin: point(stripe_x, o.y + (hh - px(20.0)) * 0.5),
size: size(px(3.0), px(20.0)) }, self.accent)
.corner_radii(px(2.0)),
);
// Status dot
let dot_color = match self.status {
AgentStatus::Running => if self.blink_visible.as_ref()
.map(|b| b.load(Ordering::Relaxed)).unwrap_or(true)
{ theme::GREEN } else { theme::SURFACE1 },
AgentStatus::Idle => theme::OVERLAY0,
AgentStatus::Done => theme::BLUE,
AgentStatus::Error => theme::RED,
};
let dot_x = stripe_x + px(11.0);
window.paint_quad(
fill(Bounds { origin: point(dot_x, o.y + (hh - px(8.0)) * 0.5),
size: size(px(8.0), px(8.0)) }, dot_color)
.corner_radii(px(4.0)),
);
// Name text
let ts = window.text_system().clone();
if !self.name.contains('\n') {
let run = TextRun { len: self.name.len(), font: font(".SystemUIFont"),
color: theme::TEXT.into(), background_color: None,
underline: None, strikethrough: None };
let shaped = ts.shape_line(self.name.clone(), px(13.0), &[run], None);
let _ = shaped.paint(point(dot_x + px(12.0), o.y + (hh - px(13.0)) * 0.5), hh, window, cx);
}
// CWD text (right-aligned)
if !self.cwd.contains('\n') {
let run = TextRun { len: self.cwd.len(), font: font(".SystemUIFont"),
color: theme::OVERLAY0.into(), background_color: None,
underline: None, strikethrough: None };
let shaped = ts.shape_line(self.cwd.clone(), px(10.0), &[run], None);
let cwd_x = o.x + w - shaped.width - px(12.0);
let _ = shaped.paint(point(cwd_x, o.y + (hh - px(10.0)) * 0.5), hh, window, cx);
}
// ── Tab Bar ──
let tab_y = o.y + hh;
// Tab bar background
window.paint_quad(fill(
Bounds { origin: point(o.x, tab_y), size: size(w, tbh) },
theme::MANTLE,
));
// Tab bar bottom border
window.paint_quad(fill(
Bounds { origin: point(o.x, tab_y + tbh - px(1.0)), size: size(w, px(1.0)) },
theme::SURFACE0,
));
// Tab labels
let mut tab_x = o.x + px(8.0);
for (i, label) in TAB_LABELS.iter().enumerate() {
let active = i == self.active_tab;
let color: Hsla = if active { self.accent.into() } else { theme::SUBTEXT0.into() };
let run = TextRun { len: label.len(), font: font(".SystemUIFont"),
color, background_color: None,
underline: None, strikethrough: None };
let shaped = ts.shape_line(SharedString::from(*label), px(11.0), &[run], None);
let label_y = tab_y + (tbh - px(11.0)) * 0.5;
let _ = shaped.paint(point(tab_x, label_y), tbh, window, cx);
// Active indicator (2px underline)
if active {
window.paint_quad(fill(
Bounds { origin: point(tab_x, tab_y + tbh - px(3.0)),
size: size(shaped.width, px(2.0)) },
self.accent,
));
}
tab_x = tab_x + shaped.width + px(12.0);
}
paint_chrome(bounds, &self.name, &self.cwd, self.accent, self.status,
&self.blink_visible, self.active_tab, window, cx);
}
}
// ── ProjectBoxFullElement ─────────────────────────────────────────────────────
/// Single custom Element for the entire ProjectBox card (header + tabs + content).
/// ProjectBox::render() returns this — no outer root div needed.
pub struct ProjectBoxFullElement {
pub id: ElementId,
pub name: SharedString,
pub cwd: SharedString,
pub accent: Rgba,
pub status: AgentStatus,
pub blink_visible: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
pub active_tab: usize,
/// Content area div (AgentPane + TerminalView or placeholder). Moved out in
/// request_layout and carried as RequestLayoutState through to paint.
pub content: AnyElement,
}
impl IntoElement for ProjectBoxFullElement {
type Element = Self;
fn into_element(self) -> Self { self }
}
impl Element for ProjectBoxFullElement {
/// Content AnyElement carried from request_layout → prepaint → paint.
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> { Some(self.id.clone()) }
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None }
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_iid: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, AnyElement) {
// Move content out of self so we can call request_layout on it (needs &mut self).
let mut content = Empty.into_any_element();
std::mem::swap(&mut content, &mut self.content);
let child_id = content.request_layout(window, cx);
// Root style: fill parent, flex-col (chrome occupies fixed top portion via paint).
let style = Style {
size: size(relative(1.0).into(), relative(1.0).into()),
flex_shrink: 0.0,
display: Display::Flex,
flex_direction: FlexDirection::Column,
..Style::default()
};
(window.request_layout(style, [child_id], cx), content)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_iid: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
content: &mut AnyElement,
window: &mut Window,
cx: &mut App,
) -> () {
// Place content below the painted chrome (header + tab bar).
let content_origin = point(bounds.origin.x, bounds.origin.y + px(CHROME_HEIGHT));
content.prepaint_at(content_origin, window, cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_iid: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
content: &mut AnyElement,
_pp: &mut (),
window: &mut Window,
cx: &mut App,
) {
// 1. Paint header + tabs directly.
paint_chrome(bounds, &self.name, &self.cwd, self.accent, self.status,
&self.blink_visible, self.active_tab, window, cx);
// 2. Paint the content child (already prepainted at correct origin).
content.paint(window, cx);
}
}