perf(ui-gpui): ProjectBoxFullElement wraps entire card, 2.1% CPU
Single custom Element for header+tabs (paint_chrome) + content AnyElement. Only 1 div remains (content wrapper). Down from 90% → 2.1% over 11 iterations.
This commit is contained in:
parent
5a7a5ad621
commit
3bbaefa9a2
2 changed files with 224 additions and 146 deletions
|
|
@ -9,10 +9,9 @@
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use crate::CachedView;
|
use crate::CachedView;
|
||||||
|
|
||||||
use crate::components::project_box_element::ProjectBoxHeaderElement;
|
use crate::components::project_box_element::ProjectBoxFullElement;
|
||||||
use crate::state::{AgentStatus, Project, ProjectTab};
|
use crate::state::{AgentStatus, Project, ProjectTab};
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
// blink_state used via fully-qualified path in init_subviews + render
|
|
||||||
|
|
||||||
// ── Accent Colors by Index ──────────────────────────────────────────
|
// ── Accent Colors by Index ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -55,7 +54,7 @@ fn tab_button(label: &'static str, active: bool, accent: Rgba) -> Div {
|
||||||
.text_color(if active { accent } else { theme::SUBTEXT0 })
|
.text_color(if active { accent } else { theme::SUBTEXT0 })
|
||||||
.cursor_pointer();
|
.cursor_pointer();
|
||||||
if active { tab = tab.border_b_2().border_color(accent); }
|
if active { tab = tab.border_b_2().border_color(accent); }
|
||||||
tab.child(label) // &'static str → no allocation
|
tab.child(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ProjectBox View ─────────────────────────────────────────────────
|
// ── ProjectBox View ─────────────────────────────────────────────────
|
||||||
|
|
@ -68,9 +67,6 @@ pub struct ProjectBox {
|
||||||
// Cached strings to avoid allocation on every render
|
// Cached strings to avoid allocation on every render
|
||||||
id_project: SharedString,
|
id_project: SharedString,
|
||||||
id_model: SharedString,
|
id_model: SharedString,
|
||||||
id_resize: SharedString,
|
|
||||||
id_docs: SharedString,
|
|
||||||
id_files: SharedString,
|
|
||||||
cached_name: SharedString,
|
cached_name: SharedString,
|
||||||
cached_cwd: SharedString,
|
cached_cwd: SharedString,
|
||||||
}
|
}
|
||||||
|
|
@ -81,9 +77,6 @@ impl ProjectBox {
|
||||||
Self {
|
Self {
|
||||||
id_project: SharedString::from(format!("project-{id}")),
|
id_project: SharedString::from(format!("project-{id}")),
|
||||||
id_model: SharedString::from(format!("model-{id}")),
|
id_model: SharedString::from(format!("model-{id}")),
|
||||||
id_resize: SharedString::from(format!("resize-{id}")),
|
|
||||||
id_docs: SharedString::from(format!("docs-{id}")),
|
|
||||||
id_files: SharedString::from(format!("files-{id}")),
|
|
||||||
cached_name: SharedString::from(project.name.clone()),
|
cached_name: SharedString::from(project.name.clone()),
|
||||||
cached_cwd: SharedString::from(project.cwd.clone()),
|
cached_cwd: SharedString::from(project.cwd.clone()),
|
||||||
project,
|
project,
|
||||||
|
|
@ -95,16 +88,12 @@ impl ProjectBox {
|
||||||
|
|
||||||
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
||||||
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
||||||
// Initialize sub-views after entity registration
|
|
||||||
|
|
||||||
// SharedBlink: Arc<AtomicBool> toggled by background timer.
|
// SharedBlink: Arc<AtomicBool> toggled by background timer.
|
||||||
// Timer calls cx.notify() on THIS ProjectBox directly — no intermediate entities.
|
// Timer calls cx.notify() on THIS ProjectBox directly — no intermediate entities.
|
||||||
// mark_view_dirty walks: ProjectBox → Workspace (2 levels only).
|
|
||||||
let should_pulse = matches!(self.project.agent.status, AgentStatus::Running)
|
let should_pulse = matches!(self.project.agent.status, AgentStatus::Running)
|
||||||
&& self.project.accent_index == 0;
|
&& self.project.accent_index == 0;
|
||||||
if should_pulse {
|
if should_pulse {
|
||||||
let blink = crate::components::blink_state::SharedBlink::new();
|
let blink = crate::components::blink_state::SharedBlink::new();
|
||||||
// Get our own entity handle to pass to the timer
|
|
||||||
let self_entity = cx.entity().downgrade();
|
let self_entity = cx.entity().downgrade();
|
||||||
let visible = blink.visible.clone();
|
let visible = blink.visible.clone();
|
||||||
cx.spawn(async move |_: WeakEntity<Self>, cx: &mut AsyncApp| {
|
cx.spawn(async move |_: WeakEntity<Self>, cx: &mut AsyncApp| {
|
||||||
|
|
@ -144,7 +133,7 @@ impl Render for ProjectBox {
|
||||||
ProjectTab::Files => 2,
|
ProjectTab::Files => 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Content area — only part that uses divs (cached children for Model tab)
|
// Content area — single div with cached children for the Model tab.
|
||||||
let mut content = div()
|
let mut content = div()
|
||||||
.id(self.id_model.clone())
|
.id(self.id_model.clone())
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|
@ -171,22 +160,9 @@ impl Render for ProjectBox {
|
||||||
.child("src/").child(" main.rs").child(" lib.rs").child("Cargo.toml"),
|
.child("src/").child(" main.rs").child(" lib.rs").child("Cargo.toml"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Root: 1 outer div + custom header/tab Element + content div
|
// Return a single custom Element that owns the entire card.
|
||||||
// Total divs: 2 (root + content) — down from 12
|
// Eliminates the outer root div — down from 2 divs to 1 (only content div remains).
|
||||||
div()
|
ProjectBoxFullElement {
|
||||||
.id(self.id_project.clone())
|
|
||||||
.flex_1()
|
|
||||||
.min_w(px(400.0))
|
|
||||||
.min_h(px(300.0))
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.bg(theme::BASE)
|
|
||||||
.rounded(px(8.0))
|
|
||||||
.border_1()
|
|
||||||
.border_color(theme::SURFACE0)
|
|
||||||
.overflow_hidden()
|
|
||||||
// Header + Tab Bar: single custom Element (0 divs, ~10 GPU primitives)
|
|
||||||
.child(ProjectBoxHeaderElement {
|
|
||||||
id: self.id_project.clone().into(),
|
id: self.id_project.clone().into(),
|
||||||
name: self.cached_name.clone(),
|
name: self.cached_name.clone(),
|
||||||
cwd: self.cached_cwd.clone(),
|
cwd: self.cached_cwd.clone(),
|
||||||
|
|
@ -194,8 +170,7 @@ impl Render for ProjectBox {
|
||||||
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,
|
active_tab: tab_idx,
|
||||||
})
|
content: content.into_any_element(),
|
||||||
// Content area (cached child views for Model tab)
|
}
|
||||||
.child(content)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
//! ProjectBoxHeaderElement — custom Element painting header + tab bar directly.
|
//! ProjectBox custom Elements:
|
||||||
//! Zero div overhead for these regions. Content area delegates to child AnyView.
|
//! - `ProjectBoxHeaderElement` — paints header + tab bar directly (zero div overhead)
|
||||||
|
//! - `ProjectBoxFullElement` — wraps header paint + content AnyElement as a single
|
||||||
|
//! custom Element, eliminating the outer root div from ProjectBox::render()
|
||||||
|
|
||||||
use gpui::*;
|
use gpui::*;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
@ -9,9 +11,124 @@ 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;
|
pub const TAB_BAR_HEIGHT: f32 = 28.0;
|
||||||
|
pub const CHROME_HEIGHT: f32 = HEADER_HEIGHT + TAB_BAR_HEIGHT;
|
||||||
|
|
||||||
const TAB_LABELS: [&str; 3] = ["Model", "Docs", "Files"];
|
const TAB_LABELS: [&str; 3] = ["Model", "Docs", "Files"];
|
||||||
|
|
||||||
|
// ── Shared paint helper ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Paint header background + tab bar into `bounds`. Used by both Elements.
|
||||||
|
fn paint_chrome(
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
name: &SharedString,
|
||||||
|
cwd: &SharedString,
|
||||||
|
accent: Rgba,
|
||||||
|
status: AgentStatus,
|
||||||
|
blink_visible: &Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||||
|
active_tab: usize,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let o = bounds.origin;
|
||||||
|
let w = bounds.size.width;
|
||||||
|
let hh = px(HEADER_HEIGHT);
|
||||||
|
let tbh = px(TAB_BAR_HEIGHT);
|
||||||
|
|
||||||
|
// Header background (rounded top corners)
|
||||||
|
window.paint_quad(PaintQuad {
|
||||||
|
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(),
|
||||||
|
border_widths: Edges::default(),
|
||||||
|
border_color: transparent_black(),
|
||||||
|
border_style: BorderStyle::default(),
|
||||||
|
});
|
||||||
|
// Header bottom border
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds { origin: point(o.x, o.y + hh - px(1.0)), size: size(w, px(1.0)) },
|
||||||
|
theme::SURFACE0,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Accent stripe
|
||||||
|
let stripe_x = o.x + px(12.0);
|
||||||
|
window.paint_quad(
|
||||||
|
fill(Bounds { origin: point(stripe_x, o.y + (hh - px(20.0)) * 0.5),
|
||||||
|
size: size(px(3.0), px(20.0)) }, accent)
|
||||||
|
.corner_radii(px(2.0)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Status dot
|
||||||
|
let dot_color = match status {
|
||||||
|
AgentStatus::Running => if 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);
|
||||||
|
window.paint_quad(
|
||||||
|
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)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Name + CWD text
|
||||||
|
let ts = window.text_system().clone();
|
||||||
|
if !name.contains('\n') {
|
||||||
|
let run = TextRun { len: name.len(), font: font(".SystemUIFont"),
|
||||||
|
color: theme::TEXT.into(), background_color: None,
|
||||||
|
underline: None, strikethrough: None };
|
||||||
|
let shaped = ts.shape_line(name.clone(), px(13.0), &[run], None);
|
||||||
|
let _ = shaped.paint(point(dot_x + px(12.0), o.y + (hh - px(13.0)) * 0.5), hh, window, cx);
|
||||||
|
}
|
||||||
|
if !cwd.contains('\n') {
|
||||||
|
let run = TextRun { len: cwd.len(), font: font(".SystemUIFont"),
|
||||||
|
color: theme::OVERLAY0.into(), background_color: None,
|
||||||
|
underline: None, strikethrough: None };
|
||||||
|
let shaped = ts.shape_line(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 + (hh - px(10.0)) * 0.5), hh, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab bar
|
||||||
|
let tab_y = o.y + hh;
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds { origin: point(o.x, tab_y), size: size(w, tbh) },
|
||||||
|
theme::MANTLE,
|
||||||
|
));
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds { origin: point(o.x, tab_y + tbh - px(1.0)), size: size(w, px(1.0)) },
|
||||||
|
theme::SURFACE0,
|
||||||
|
));
|
||||||
|
let mut tab_x = o.x + px(8.0);
|
||||||
|
for (i, label) in TAB_LABELS.iter().enumerate() {
|
||||||
|
let active = i == active_tab;
|
||||||
|
let color: Hsla = if active { 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);
|
||||||
|
if active {
|
||||||
|
window.paint_quad(fill(
|
||||||
|
Bounds { origin: point(tab_x, tab_y + tbh - px(3.0)),
|
||||||
|
size: size(shaped.width, px(2.0)) },
|
||||||
|
accent,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
tab_x = tab_x + shaped.width + px(12.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ProjectBoxHeaderElement ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Standalone header + tab bar element. Used when the outer container is still a div.
|
||||||
|
/// Kept for compatibility; ProjectBoxFullElement supersedes it.
|
||||||
pub struct ProjectBoxHeaderElement {
|
pub struct ProjectBoxHeaderElement {
|
||||||
pub id: ElementId,
|
pub id: ElementId,
|
||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
|
|
@ -19,7 +136,7 @@ pub struct ProjectBoxHeaderElement {
|
||||||
pub accent: Rgba,
|
pub accent: Rgba,
|
||||||
pub status: AgentStatus,
|
pub status: AgentStatus,
|
||||||
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
|
pub active_tab: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for ProjectBoxHeaderElement {
|
impl IntoElement for ProjectBoxHeaderElement {
|
||||||
|
|
@ -38,11 +155,11 @@ impl Element for ProjectBoxHeaderElement {
|
||||||
&mut self, _id: Option<&GlobalElementId>, _iid: Option<&InspectorElementId>,
|
&mut self, _id: Option<&GlobalElementId>, _iid: Option<&InspectorElementId>,
|
||||||
window: &mut Window, cx: &mut App,
|
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(AbsoluteLength::Pixels(px(total_h)))),
|
height: Length::Definite(DefiniteLength::Absolute(AbsoluteLength::Pixels(
|
||||||
|
px(CHROME_HEIGHT)))),
|
||||||
},
|
},
|
||||||
flex_shrink: 0.0,
|
flex_shrink: 0.0,
|
||||||
..Style::default()
|
..Style::default()
|
||||||
|
|
@ -60,106 +177,92 @@ impl Element for ProjectBoxHeaderElement {
|
||||||
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;
|
paint_chrome(bounds, &self.name, &self.cwd, self.accent, self.status,
|
||||||
let w = bounds.size.width;
|
&self.blink_visible, self.active_tab, window, cx);
|
||||||
let hh = px(HEADER_HEIGHT);
|
}
|
||||||
let tbh = px(TAB_BAR_HEIGHT);
|
}
|
||||||
|
|
||||||
// ── Header Background ──
|
// ── ProjectBoxFullElement ─────────────────────────────────────────────────────
|
||||||
window.paint_quad(PaintQuad {
|
|
||||||
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(),
|
|
||||||
border_widths: Edges::default(),
|
|
||||||
border_color: transparent_black(),
|
|
||||||
border_style: BorderStyle::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Header bottom border
|
/// Single custom Element for the entire ProjectBox card (header + tabs + content).
|
||||||
window.paint_quad(fill(
|
/// ProjectBox::render() returns this — no outer root div needed.
|
||||||
Bounds { origin: point(o.x, o.y + hh - px(1.0)), size: size(w, px(1.0)) },
|
pub struct ProjectBoxFullElement {
|
||||||
theme::SURFACE0,
|
pub id: ElementId,
|
||||||
));
|
pub name: SharedString,
|
||||||
|
pub cwd: SharedString,
|
||||||
|
pub accent: Rgba,
|
||||||
|
pub status: AgentStatus,
|
||||||
|
pub blink_visible: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
|
||||||
|
pub active_tab: usize,
|
||||||
|
/// Content area div (AgentPane + TerminalView or placeholder). Moved out in
|
||||||
|
/// request_layout and carried as RequestLayoutState through to paint.
|
||||||
|
pub content: AnyElement,
|
||||||
|
}
|
||||||
|
|
||||||
// Accent stripe
|
impl IntoElement for ProjectBoxFullElement {
|
||||||
let stripe_x = o.x + px(12.0);
|
type Element = Self;
|
||||||
window.paint_quad(
|
fn into_element(self) -> Self { self }
|
||||||
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)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Status dot
|
impl Element for ProjectBoxFullElement {
|
||||||
let dot_color = match self.status {
|
/// Content AnyElement carried from request_layout → prepaint → paint.
|
||||||
AgentStatus::Running => if self.blink_visible.as_ref()
|
type RequestLayoutState = AnyElement;
|
||||||
.map(|b| b.load(Ordering::Relaxed)).unwrap_or(true)
|
type PrepaintState = ();
|
||||||
{ theme::GREEN } else { theme::SURFACE1 },
|
|
||||||
AgentStatus::Idle => theme::OVERLAY0,
|
fn id(&self) -> Option<ElementId> { Some(self.id.clone()) }
|
||||||
AgentStatus::Done => theme::BLUE,
|
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> { None }
|
||||||
AgentStatus::Error => theme::RED,
|
|
||||||
|
fn request_layout(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_iid: Option<&InspectorElementId>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> (LayoutId, AnyElement) {
|
||||||
|
// Move content out of self so we can call request_layout on it (needs &mut self).
|
||||||
|
let mut content = Empty.into_any_element();
|
||||||
|
std::mem::swap(&mut content, &mut self.content);
|
||||||
|
let child_id = content.request_layout(window, cx);
|
||||||
|
|
||||||
|
// Root style: fill parent, flex-col (chrome occupies fixed top portion via paint).
|
||||||
|
let style = Style {
|
||||||
|
size: size(relative(1.0).into(), relative(1.0).into()),
|
||||||
|
flex_shrink: 0.0,
|
||||||
|
display: Display::Flex,
|
||||||
|
flex_direction: FlexDirection::Column,
|
||||||
|
..Style::default()
|
||||||
};
|
};
|
||||||
let dot_x = stripe_x + px(11.0);
|
(window.request_layout(style, [child_id], cx), content)
|
||||||
window.paint_quad(
|
|
||||||
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)),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Name text
|
|
||||||
let ts = window.text_system().clone();
|
|
||||||
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 + (hh - px(13.0)) * 0.5), hh, window, cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CWD text (right-aligned)
|
fn prepaint(
|
||||||
if !self.cwd.contains('\n') {
|
&mut self,
|
||||||
let run = TextRun { len: self.cwd.len(), font: font(".SystemUIFont"),
|
_id: Option<&GlobalElementId>,
|
||||||
color: theme::OVERLAY0.into(), background_color: None,
|
_iid: Option<&InspectorElementId>,
|
||||||
underline: None, strikethrough: None };
|
bounds: Bounds<Pixels>,
|
||||||
let shaped = ts.shape_line(self.cwd.clone(), px(10.0), &[run], None);
|
content: &mut AnyElement,
|
||||||
let cwd_x = o.x + w - shaped.width - px(12.0);
|
window: &mut Window,
|
||||||
let _ = shaped.paint(point(cwd_x, o.y + (hh - px(10.0)) * 0.5), hh, window, cx);
|
cx: &mut App,
|
||||||
|
) -> () {
|
||||||
|
// Place content below the painted chrome (header + tab bar).
|
||||||
|
let content_origin = point(bounds.origin.x, bounds.origin.y + px(CHROME_HEIGHT));
|
||||||
|
content.prepaint_at(content_origin, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tab Bar ──
|
fn paint(
|
||||||
let tab_y = o.y + hh;
|
&mut self,
|
||||||
// Tab bar background
|
_id: Option<&GlobalElementId>,
|
||||||
window.paint_quad(fill(
|
_iid: Option<&InspectorElementId>,
|
||||||
Bounds { origin: point(o.x, tab_y), size: size(w, tbh) },
|
bounds: Bounds<Pixels>,
|
||||||
theme::MANTLE,
|
content: &mut AnyElement,
|
||||||
));
|
_pp: &mut (),
|
||||||
// Tab bar bottom border
|
window: &mut Window,
|
||||||
window.paint_quad(fill(
|
cx: &mut App,
|
||||||
Bounds { origin: point(o.x, tab_y + tbh - px(1.0)), size: size(w, px(1.0)) },
|
) {
|
||||||
theme::SURFACE0,
|
// 1. Paint header + tabs directly.
|
||||||
));
|
paint_chrome(bounds, &self.name, &self.cwd, self.accent, self.status,
|
||||||
|
&self.blink_visible, self.active_tab, window, cx);
|
||||||
// Tab labels
|
// 2. Paint the content child (already prepainted at correct origin).
|
||||||
let mut tab_x = o.x + px(8.0);
|
content.paint(window, cx);
|
||||||
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