diff --git a/ui-gpui/src/components/mod.rs b/ui-gpui/src/components/mod.rs index be44a43..84afbca 100644 --- a/ui-gpui/src/components/mod.rs +++ b/ui-gpui/src/components/mod.rs @@ -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; diff --git a/ui-gpui/src/components/project_box.rs b/ui-gpui/src/components/project_box.rs index a8f6fe4..dff179d 100644 --- a/ui-gpui/src/components/project_box.rs +++ b/ui-gpui/src/components/project_box.rs @@ -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() diff --git a/ui-gpui/src/components/project_box_element.rs b/ui-gpui/src/components/project_box_element.rs new file mode 100644 index 0000000..7b1ca79 --- /dev/null +++ b/ui-gpui/src/components/project_box_element.rs @@ -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>, +} + +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 { 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, _rl: &mut (), _window: &mut Window, _cx: &mut App, + ) {} + + fn paint( + &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&InspectorElementId>, + bounds: Bounds, _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); + } + } +}