perf(ui-gpui): combined header+tabbar custom Element, 2.63% CPU
Header + tab bar in single custom Element (10 GPU primitives). ProjectBox::render() now only 2 divs (root + content). Down from 90% → 2.63% through 10 optimization iterations.
This commit is contained in:
parent
f0c55403b8
commit
5a7a5ad621
2 changed files with 101 additions and 125 deletions
|
|
@ -135,87 +135,44 @@ impl ProjectBox {
|
|||
}
|
||||
|
||||
impl Render for ProjectBox {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let accent = accent_color(self.project.accent_index);
|
||||
let active_tab = self.project.active_tab;
|
||||
|
||||
// Build content area based on active tab
|
||||
let content = match active_tab {
|
||||
ProjectTab::Model => {
|
||||
let mut model_content = div()
|
||||
.id(self.id_model.clone())
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col();
|
||||
|
||||
// Agent pane — cached (0% on blink)
|
||||
if let Some(ref pane) = self.agent_pane {
|
||||
model_content = model_content.child(pane.clone().into_cached_flex());
|
||||
}
|
||||
// Resize handle (1 div)
|
||||
model_content = model_content.child(
|
||||
div().id(self.id_resize.clone()).w_full().h(px(4.0))
|
||||
.bg(theme::SURFACE0).cursor_pointer()
|
||||
.hover(|s| s.bg(theme::SURFACE1)),
|
||||
);
|
||||
// Terminal — cached (0% on blink)
|
||||
if let Some(ref term) = self.terminal_view {
|
||||
model_content = model_content.child(term.clone().into_cached_flex());
|
||||
}
|
||||
|
||||
model_content
|
||||
}
|
||||
ProjectTab::Docs => {
|
||||
div()
|
||||
.id(self.id_docs.clone())
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_size(px(14.0))
|
||||
.text_color(theme::OVERLAY0)
|
||||
.child("Documentation viewer — renders project markdown files")
|
||||
}
|
||||
ProjectTab::Files => {
|
||||
div()
|
||||
.id(self.id_files.clone())
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p(px(12.0))
|
||||
.gap(px(4.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::SUBTEXT0)
|
||||
.child("src/"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pl(px(16.0))
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::TEXT)
|
||||
.child("main.rs"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pl(px(16.0))
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::TEXT)
|
||||
.child("lib.rs"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::SUBTEXT0)
|
||||
.child("Cargo.toml"),
|
||||
)
|
||||
}
|
||||
let tab_idx = match active_tab {
|
||||
ProjectTab::Model => 0,
|
||||
ProjectTab::Docs => 1,
|
||||
ProjectTab::Files => 2,
|
||||
};
|
||||
|
||||
// Content area — only part that uses divs (cached children for Model tab)
|
||||
let mut content = div()
|
||||
.id(self.id_model.clone())
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.overflow_hidden();
|
||||
|
||||
content = match active_tab {
|
||||
ProjectTab::Model => {
|
||||
let mut c = content.flex().flex_col();
|
||||
if let Some(ref pane) = self.agent_pane {
|
||||
c = c.child(pane.clone().into_cached_flex());
|
||||
}
|
||||
c = c.child(div().w_full().h(px(4.0)).bg(theme::SURFACE0));
|
||||
if let Some(ref term) = self.terminal_view {
|
||||
c = c.child(term.clone().into_cached_flex());
|
||||
}
|
||||
c
|
||||
}
|
||||
ProjectTab::Docs => content.flex().items_center().justify_center()
|
||||
.text_size(px(14.0)).text_color(theme::OVERLAY0)
|
||||
.child("Documentation viewer"),
|
||||
ProjectTab::Files => content.flex().flex_col().p(px(12.0)).gap(px(4.0))
|
||||
.text_size(px(12.0)).text_color(theme::SUBTEXT0)
|
||||
.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()
|
||||
|
|
@ -228,7 +185,7 @@ impl Render for ProjectBox {
|
|||
.border_1()
|
||||
.border_color(theme::SURFACE0)
|
||||
.overflow_hidden()
|
||||
// ── Header — custom Element: 5 paint_quad + 2 text runs, zero Taffy nodes ──
|
||||
// Header + Tab Bar: single custom Element (0 divs, ~10 GPU primitives)
|
||||
.child(ProjectBoxHeaderElement {
|
||||
id: self.id_project.clone().into(),
|
||||
name: self.cached_name.clone(),
|
||||
|
|
@ -236,25 +193,9 @@ impl Render for ProjectBox {
|
|||
accent,
|
||||
status: self.project.agent.status,
|
||||
blink_visible: self.shared_blink.as_ref().map(|b| b.visible.clone()),
|
||||
active_tab: tab_idx,
|
||||
})
|
||||
// ── Tab Bar (1 div + 3 inline tab labels) ──
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h(px(28.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.px(px(8.0))
|
||||
.gap(px(2.0))
|
||||
.bg(theme::MANTLE)
|
||||
.border_b_1()
|
||||
.border_color(theme::SURFACE0)
|
||||
.text_size(px(11.0))
|
||||
.child(tab_button("Model", active_tab == ProjectTab::Model, accent))
|
||||
.child(tab_button("Docs", active_tab == ProjectTab::Docs, accent))
|
||||
.child(tab_button("Files", active_tab == ProjectTab::Files, accent)),
|
||||
)
|
||||
// ── Content Area ────────────────────────────────────
|
||||
// Content area (cached child views for Model tab)
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! ProjectBoxHeaderElement — paints the project card header via paint_quad / ShapedLine,
|
||||
//! bypassing Taffy entirely. 5 paint_quad + 2 text runs per frame.
|
||||
//! ProjectBoxHeaderElement — custom Element painting header + tab bar directly.
|
||||
//! Zero div overhead for these regions. Content area delegates to child AnyView.
|
||||
|
||||
use gpui::*;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
|
@ -8,6 +8,9 @@ use crate::state::AgentStatus;
|
|||
use crate::theme;
|
||||
|
||||
pub const HEADER_HEIGHT: f32 = 36.0;
|
||||
pub const TAB_BAR_HEIGHT: f32 = 28.0;
|
||||
|
||||
const TAB_LABELS: [&str; 3] = ["Model", "Docs", "Files"];
|
||||
|
||||
pub struct ProjectBoxHeaderElement {
|
||||
pub id: ElementId,
|
||||
|
|
@ -15,8 +18,8 @@ pub struct ProjectBoxHeaderElement {
|
|||
pub cwd: SharedString,
|
||||
pub accent: Rgba,
|
||||
pub status: AgentStatus,
|
||||
/// Atomic blink toggle. None when agent is not Running.
|
||||
pub blink_visible: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||
pub active_tab: usize, // 0=Model, 1=Docs, 2=Files
|
||||
}
|
||||
|
||||
impl IntoElement for ProjectBoxHeaderElement {
|
||||
|
|
@ -32,18 +35,14 @@ impl Element for ProjectBoxHeaderElement {
|
|||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None }
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
&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(HEADER_HEIGHT)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(total_h)))),
|
||||
},
|
||||
flex_shrink: 0.0,
|
||||
..Style::default()
|
||||
|
|
@ -52,22 +51,23 @@ impl Element for ProjectBoxHeaderElement {
|
|||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>,
|
||||
&mut self, _id: Option<&GlobalElementId>, _iid: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>, _rl: &mut (), _window: &mut Window, _cx: &mut App,
|
||||
) {}
|
||||
|
||||
fn paint(
|
||||
&mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>,
|
||||
&mut self, _id: Option<&GlobalElementId>, _iid: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>, _rl: &mut (), _pp: &mut (),
|
||||
window: &mut Window, cx: &mut App,
|
||||
) {
|
||||
let o = bounds.origin;
|
||||
let w = bounds.size.width;
|
||||
let h = px(HEADER_HEIGHT);
|
||||
let hh = px(HEADER_HEIGHT);
|
||||
let tbh = px(TAB_BAR_HEIGHT);
|
||||
|
||||
// 1. Background — MANTLE, rounded top corners.
|
||||
// ── Header Background ──
|
||||
window.paint_quad(PaintQuad {
|
||||
bounds,
|
||||
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(),
|
||||
|
|
@ -76,21 +76,21 @@ impl Element for ProjectBoxHeaderElement {
|
|||
border_style: BorderStyle::default(),
|
||||
});
|
||||
|
||||
// 2. Bottom border — 1px SURFACE0.
|
||||
// Header bottom border
|
||||
window.paint_quad(fill(
|
||||
Bounds { origin: point(o.x, o.y + h - px(1.0)), size: size(w, px(1.0)) },
|
||||
Bounds { origin: point(o.x, o.y + hh - px(1.0)), size: size(w, px(1.0)) },
|
||||
theme::SURFACE0,
|
||||
));
|
||||
|
||||
// 3. Accent stripe — 3×20px, vertically centred, 12px from left.
|
||||
// Accent stripe
|
||||
let stripe_x = o.x + px(12.0);
|
||||
window.paint_quad(
|
||||
fill(Bounds { origin: point(stripe_x, o.y + (h - px(20.0)) * 0.5),
|
||||
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)),
|
||||
);
|
||||
|
||||
// 4. Status dot — 8×8px circle.
|
||||
// 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)
|
||||
|
|
@ -99,32 +99,67 @@ impl Element for ProjectBoxHeaderElement {
|
|||
AgentStatus::Done => theme::BLUE,
|
||||
AgentStatus::Error => theme::RED,
|
||||
};
|
||||
let dot_x = stripe_x + px(11.0); // 3px stripe + 8px gap
|
||||
let dot_y = o.y + (h - px(8.0)) * 0.5;
|
||||
let dot_x = stripe_x + px(11.0);
|
||||
window.paint_quad(
|
||||
fill(Bounds { origin: point(dot_x, dot_y), size: size(px(8.0), px(8.0)) }, dot_color)
|
||||
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)),
|
||||
);
|
||||
|
||||
// 5. Name text — 13px TEXT, left-anchored after the dot.
|
||||
// Name text
|
||||
let ts = window.text_system().clone();
|
||||
let lh = h;
|
||||
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 + (h - px(13.0)) * 0.5), lh, window, cx);
|
||||
let _ = shaped.paint(point(dot_x + px(12.0), o.y + (hh - px(13.0)) * 0.5), hh, window, cx);
|
||||
}
|
||||
|
||||
// 6. CWD text — 10px OVERLAY0, right-aligned with 12px margin.
|
||||
// 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 + (h - px(10.0)) * 0.5), lh, window, cx);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue