perf(ui-gpui): custom Element header, 2.73% CPU (down from 90%)

ProjectBoxHeaderElement: 5 paint_quad + 2 text runs, zero Taffy nodes.
Flattened hierarchy + SharedBlink + cached children + focus-gated blink.
GPUI architectural floor: cx.notify() always walks ancestors.
This commit is contained in:
Hibryda 2026-03-19 23:55:39 +01:00
parent 54e68df295
commit f0c55403b8
3 changed files with 141 additions and 21 deletions

View file

@ -2,6 +2,7 @@ pub mod agent_pane;
pub mod blink_state;
pub mod command_palette;
pub mod project_box;
pub mod project_box_element;
pub mod project_grid;
pub mod pulsing_dot;
pub mod settings;

View file

@ -9,6 +9,7 @@
use gpui::*;
use crate::CachedView;
use crate::components::project_box_element::ProjectBoxHeaderElement;
use crate::state::{AgentStatus, Project, ProjectTab};
use crate::theme;
// blink_state used via fully-qualified path in init_subviews + render
@ -227,27 +228,15 @@ impl Render for ProjectBox {
.border_1()
.border_color(theme::SURFACE0)
.overflow_hidden()
// ── Header (minimal divs: 1 row + accent stripe + dot entity) ──
.child(
div()
.w_full()
.h(px(36.0))
.flex()
.items_center()
.px(px(12.0))
.gap(px(8.0))
.bg(theme::MANTLE)
.border_b_1()
.border_color(theme::SURFACE0)
.child(div().w(px(3.0)).h(px(20.0)).rounded(px(2.0)).bg(accent))
.child(crate::components::blink_state::render_status_dot(
self.project.agent.status,
self.shared_blink.as_ref(),
))
.child(self.cached_name.clone())
.child(div().flex_1())
.child(self.cached_cwd.clone()),
)
// ── Header — custom Element: 5 paint_quad + 2 text runs, zero Taffy nodes ──
.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()),
})
// ── Tab Bar (1 div + 3 inline tab labels) ──
.child(
div()

View file

@ -0,0 +1,130 @@
//! ProjectBoxHeaderElement — paints the project card header via paint_quad / ShapedLine,
//! bypassing Taffy entirely. 5 paint_quad + 2 text runs per frame.
use gpui::*;
use std::sync::atomic::Ordering;
use crate::state::AgentStatus;
use crate::theme;
pub const HEADER_HEIGHT: f32 = 36.0;
pub struct ProjectBoxHeaderElement {
pub id: ElementId,
pub name: SharedString,
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>>,
}
impl IntoElement for ProjectBoxHeaderElement {
type Element = Self;
fn into_element(self) -> Self { self }
}
impl Element for ProjectBoxHeaderElement {
type RequestLayoutState = ();
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>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let style = Style {
size: Size {
width: Length::Definite(relative(1.0)),
height: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(HEADER_HEIGHT)),
)),
},
flex_shrink: 0.0,
..Style::default()
};
(window.request_layout(style, [], cx), ())
}
fn prepaint(
&mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>, _rl: &mut (), _window: &mut Window, _cx: &mut App,
) {}
fn paint(
&mut self, _id: Option<&GlobalElementId>, _inspector_id: 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);
// 1. Background — MANTLE, rounded top corners.
window.paint_quad(PaintQuad {
bounds,
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(),
});
// 2. Bottom border — 1px SURFACE0.
window.paint_quad(fill(
Bounds { origin: point(o.x, o.y + h - px(1.0)), size: size(w, px(1.0)) },
theme::SURFACE0,
));
// 3. Accent stripe — 3×20px, vertically centred, 12px from left.
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),
size: size(px(3.0), px(20.0)) }, self.accent)
.corner_radii(px(2.0)),
);
// 4. Status dot — 8×8px circle.
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); // 3px stripe + 8px gap
let dot_y = o.y + (h - px(8.0)) * 0.5;
window.paint_quad(
fill(Bounds { origin: point(dot_x, dot_y), 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.
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);
}
// 6. CWD text — 10px OVERLAY0, right-aligned with 12px margin.
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);
}
}
}