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:
parent
54e68df295
commit
f0c55403b8
3 changed files with 141 additions and 21 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
130
ui-gpui/src/components/project_box_element.rs
Normal file
130
ui-gpui/src/components/project_box_element.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue