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 {
|
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 accent = accent_color(self.project.accent_index);
|
||||||
let active_tab = self.project.active_tab;
|
let active_tab = self.project.active_tab;
|
||||||
|
let tab_idx = match active_tab {
|
||||||
// Build content area based on active tab
|
ProjectTab::Model => 0,
|
||||||
let content = match active_tab {
|
ProjectTab::Docs => 1,
|
||||||
ProjectTab::Model => {
|
ProjectTab::Files => 2,
|
||||||
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"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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()
|
div()
|
||||||
.id(self.id_project.clone())
|
.id(self.id_project.clone())
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|
@ -228,7 +185,7 @@ impl Render for ProjectBox {
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(theme::SURFACE0)
|
.border_color(theme::SURFACE0)
|
||||||
.overflow_hidden()
|
.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 {
|
.child(ProjectBoxHeaderElement {
|
||||||
id: self.id_project.clone().into(),
|
id: self.id_project.clone().into(),
|
||||||
name: self.cached_name.clone(),
|
name: self.cached_name.clone(),
|
||||||
|
|
@ -236,25 +193,9 @@ impl Render for ProjectBox {
|
||||||
accent,
|
accent,
|
||||||
status: self.project.agent.status,
|
status: self.project.agent.status,
|
||||||
blink_visible: self.shared_blink.as_ref().map(|b| b.visible.clone()),
|
blink_visible: self.shared_blink.as_ref().map(|b| b.visible.clone()),
|
||||||
|
active_tab: tab_idx,
|
||||||
})
|
})
|
||||||
// ── Tab Bar (1 div + 3 inline tab labels) ──
|
// Content area (cached child views for Model tab)
|
||||||
.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 ────────────────────────────────────
|
|
||||||
.child(content)
|
.child(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! ProjectBoxHeaderElement — paints the project card header via paint_quad / ShapedLine,
|
//! ProjectBoxHeaderElement — custom Element painting header + tab bar directly.
|
||||||
//! bypassing Taffy entirely. 5 paint_quad + 2 text runs per frame.
|
//! Zero div overhead for these regions. Content area delegates to child AnyView.
|
||||||
|
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
@ -8,6 +8,9 @@ use crate::state::AgentStatus;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
|
|
||||||
pub const HEADER_HEIGHT: f32 = 36.0;
|
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 struct ProjectBoxHeaderElement {
|
||||||
pub id: ElementId,
|
pub id: ElementId,
|
||||||
|
|
@ -15,8 +18,8 @@ pub struct ProjectBoxHeaderElement {
|
||||||
pub cwd: SharedString,
|
pub cwd: SharedString,
|
||||||
pub accent: Rgba,
|
pub accent: Rgba,
|
||||||
pub status: AgentStatus,
|
pub status: AgentStatus,
|
||||||
/// Atomic blink toggle. None when agent is not Running.
|
|
||||||
pub blink_visible: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
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 {
|
impl IntoElement for ProjectBoxHeaderElement {
|
||||||
|
|
@ -32,18 +35,14 @@ impl Element for ProjectBoxHeaderElement {
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None }
|
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None }
|
||||||
|
|
||||||
fn request_layout(
|
fn request_layout(
|
||||||
&mut self,
|
&mut self, _id: Option<&GlobalElementId>, _iid: Option<&InspectorElementId>,
|
||||||
_id: Option<&GlobalElementId>,
|
window: &mut Window, cx: &mut App,
|
||||||
_inspector_id: Option<&InspectorElementId>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> (LayoutId, ()) {
|
) -> (LayoutId, ()) {
|
||||||
|
let total_h = HEADER_HEIGHT + TAB_BAR_HEIGHT;
|
||||||
let style = Style {
|
let style = Style {
|
||||||
size: Size {
|
size: Size {
|
||||||
width: Length::Definite(relative(1.0)),
|
width: Length::Definite(relative(1.0)),
|
||||||
height: Length::Definite(DefiniteLength::Absolute(
|
height: Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(total_h)))),
|
||||||
AbsoluteLength::Pixels(px(HEADER_HEIGHT)),
|
|
||||||
)),
|
|
||||||
},
|
},
|
||||||
flex_shrink: 0.0,
|
flex_shrink: 0.0,
|
||||||
..Style::default()
|
..Style::default()
|
||||||
|
|
@ -52,22 +51,23 @@ impl Element for ProjectBoxHeaderElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepaint(
|
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,
|
_bounds: Bounds<Pixels>, _rl: &mut (), _window: &mut Window, _cx: &mut App,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
fn paint(
|
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 (),
|
bounds: Bounds<Pixels>, _rl: &mut (), _pp: &mut (),
|
||||||
window: &mut Window, cx: &mut App,
|
window: &mut Window, cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let o = bounds.origin;
|
let o = bounds.origin;
|
||||||
let w = bounds.size.width;
|
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 {
|
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),
|
corner_radii: Corners { top_left: px(6.0), top_right: px(6.0),
|
||||||
bottom_left: px(0.0), bottom_right: px(0.0) },
|
bottom_left: px(0.0), bottom_right: px(0.0) },
|
||||||
background: theme::MANTLE.into(),
|
background: theme::MANTLE.into(),
|
||||||
|
|
@ -76,21 +76,21 @@ impl Element for ProjectBoxHeaderElement {
|
||||||
border_style: BorderStyle::default(),
|
border_style: BorderStyle::default(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Bottom border — 1px SURFACE0.
|
// Header bottom border
|
||||||
window.paint_quad(fill(
|
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,
|
theme::SURFACE0,
|
||||||
));
|
));
|
||||||
|
|
||||||
// 3. Accent stripe — 3×20px, vertically centred, 12px from left.
|
// Accent stripe
|
||||||
let stripe_x = o.x + px(12.0);
|
let stripe_x = o.x + px(12.0);
|
||||||
window.paint_quad(
|
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)
|
size: size(px(3.0), px(20.0)) }, self.accent)
|
||||||
.corner_radii(px(2.0)),
|
.corner_radii(px(2.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Status dot — 8×8px circle.
|
// Status dot
|
||||||
let dot_color = match self.status {
|
let dot_color = match self.status {
|
||||||
AgentStatus::Running => if self.blink_visible.as_ref()
|
AgentStatus::Running => if self.blink_visible.as_ref()
|
||||||
.map(|b| b.load(Ordering::Relaxed)).unwrap_or(true)
|
.map(|b| b.load(Ordering::Relaxed)).unwrap_or(true)
|
||||||
|
|
@ -99,32 +99,67 @@ impl Element for ProjectBoxHeaderElement {
|
||||||
AgentStatus::Done => theme::BLUE,
|
AgentStatus::Done => theme::BLUE,
|
||||||
AgentStatus::Error => theme::RED,
|
AgentStatus::Error => theme::RED,
|
||||||
};
|
};
|
||||||
let dot_x = stripe_x + px(11.0); // 3px stripe + 8px gap
|
let dot_x = stripe_x + px(11.0);
|
||||||
let dot_y = o.y + (h - px(8.0)) * 0.5;
|
|
||||||
window.paint_quad(
|
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)),
|
.corner_radii(px(4.0)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 5. Name text — 13px TEXT, left-anchored after the dot.
|
// Name text
|
||||||
let ts = window.text_system().clone();
|
let ts = window.text_system().clone();
|
||||||
let lh = h;
|
|
||||||
if !self.name.contains('\n') {
|
if !self.name.contains('\n') {
|
||||||
let run = TextRun { len: self.name.len(), font: font(".SystemUIFont"),
|
let run = TextRun { len: self.name.len(), font: font(".SystemUIFont"),
|
||||||
color: theme::TEXT.into(), background_color: None,
|
color: theme::TEXT.into(), background_color: None,
|
||||||
underline: None, strikethrough: None };
|
underline: None, strikethrough: None };
|
||||||
let shaped = ts.shape_line(self.name.clone(), px(13.0), &[run], 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') {
|
if !self.cwd.contains('\n') {
|
||||||
let run = TextRun { len: self.cwd.len(), font: font(".SystemUIFont"),
|
let run = TextRun { len: self.cwd.len(), font: font(".SystemUIFont"),
|
||||||
color: theme::OVERLAY0.into(), background_color: None,
|
color: theme::OVERLAY0.into(), background_color: None,
|
||||||
underline: None, strikethrough: None };
|
underline: None, strikethrough: None };
|
||||||
let shaped = ts.shape_line(self.cwd.clone(), px(10.0), &[run], 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 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