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:
parent
5a7a5ad621
commit
3bbaefa9a2
2 changed files with 224 additions and 146 deletions
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue