agent-orchestrator/ui-gpui/src/workspace.rs
Hibryda c61262c604 perf(ui-gpui): eliminate ProjectBox Entity, plain struct + direct render (2.17%)
ProjectBoxData replaces Entity<ProjectBox>. Workspace is the only view
entity in the dispatch tree. Timer notifies Workspace via cx.spawn.
12 optimization iterations: 90% → 2.17% CPU for a pulsing dot.
2026-03-20 00:40:35 +01:00

208 lines
7.4 KiB
Rust

//! Workspace: root view composing sidebar + project boxes + status bar.
//!
//! ProjectBoxes are NOT entities — they're plain structs rendered as custom Elements.
//! This eliminates the Entity boundary and dispatch tree overhead for project cards.
//! Only Workspace is a view in the dispatch tree. Blink timer notifies Workspace directly.
//! Dispatch tree depth: 1 (just Workspace). AgentPane/TerminalView are cached child entities.
use gpui::*;
use crate::components::command_palette::CommandPalette;
use crate::components::project_box::ProjectBoxData;
use crate::components::project_box_element::ProjectBoxFullElement;
use crate::components::settings::SettingsPanel;
use crate::components::sidebar::Sidebar;
use crate::components::status_bar::StatusBar;
use crate::state::{AgentStatus, AppState, ProjectTab};
use crate::theme;
use crate::CachedView;
fn accent_color(index: usize) -> Rgba {
const ACCENTS: [Rgba; 8] = [
theme::BLUE, theme::MAUVE, theme::GREEN, theme::PEACH,
theme::PINK, theme::TEAL, theme::SAPPHIRE, theme::LAVENDER,
];
ACCENTS[index % ACCENTS.len()]
}
pub struct Workspace {
#[allow(dead_code)]
app_state: Entity<AppState>,
sidebar: Entity<Sidebar>,
settings_panel: Entity<SettingsPanel>,
project_boxes: Vec<ProjectBoxData>,
status_bar: Entity<StatusBar>,
command_palette: Entity<CommandPalette>,
}
impl Workspace {
pub fn new(app_state: Entity<AppState>, cx: &mut Context<Self>) -> Self {
let sidebar = cx.new({
let state = app_state.clone();
|_cx| Sidebar::new(state)
});
let settings_panel = cx.new({
let state = app_state.clone();
|_cx| SettingsPanel::new(state)
});
let status_bar = cx.new({
let state = app_state.clone();
|_cx| StatusBar::new(state)
});
let command_palette = cx.new({
let state = app_state.clone();
|_cx| CommandPalette::new(state)
});
// Create ProjectBoxData (plain structs with entity handles for content)
let projects: Vec<_> = app_state.read(cx).projects.clone();
let project_boxes: Vec<ProjectBoxData> = projects
.into_iter()
.map(|proj| {
let mut data = ProjectBoxData::new(&proj);
// Create cached child entities for content
let agent_pane = cx.new(|_cx| {
crate::components::agent_pane::AgentPane::with_demo_messages()
});
let terminal_view = cx.new(|_cx| {
let mut tv = crate::terminal::renderer::TerminalView::new(120, 10);
tv.feed_demo();
tv
});
data.agent_pane = Some(agent_pane);
data.terminal_view = Some(terminal_view);
// Start blink timer for Running projects (focus-gated: first only)
let should_pulse = matches!(proj.agent.status, AgentStatus::Running)
&& proj.accent_index == 0;
if should_pulse {
let blink = crate::components::blink_state::SharedBlink::new();
let visible = blink.visible.clone();
// Notify WORKSPACE directly — it's the only view entity
cx.spawn(async move |workspace: WeakEntity<Self>, cx: &mut AsyncApp| {
loop {
cx.background_executor().timer(std::time::Duration::from_millis(500)).await;
visible.fetch_xor(true, std::sync::atomic::Ordering::Relaxed);
let ok = workspace.update(cx, |_, cx| cx.notify());
if ok.is_err() { break; }
}
}).detach();
data.shared_blink = Some(blink);
}
data
})
.collect();
Self {
app_state,
sidebar,
settings_panel,
project_boxes,
status_bar,
command_palette,
}
}
}
impl Render for Workspace {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let sidebar_open = true;
let settings_open = false;
let palette_open = false;
let mut root = div()
.id("workspace-root")
.size_full()
.flex()
.flex_col()
.bg(theme::CRUST)
.font_family("Inter");
let mut main_row = div()
.id("main-row")
.flex_1()
.w_full()
.flex()
.flex_row()
.overflow_hidden();
if sidebar_open {
main_row = main_row.child(self.sidebar.clone());
}
if settings_open {
main_row = main_row.child(self.settings_panel.clone());
}
// Project grid — inline custom Elements, NO entity children
let mut grid = div()
.id("project-grid")
.flex_1()
.h_full()
.flex()
.flex_row()
.flex_wrap()
.gap(px(8.0))
.p(px(8.0))
.bg(theme::CRUST)
.overflow_y_scroll();
for data in &self.project_boxes {
let accent = accent_color(data.accent_index);
let tab_idx = match data.active_tab {
ProjectTab::Model => 0,
ProjectTab::Docs => 1,
ProjectTab::Files => 2,
};
// Build content div with cached child entities
let mut content = div()
.id(data.id_content.clone())
.flex_1()
.w_full()
.overflow_hidden();
content = match data.active_tab {
ProjectTab::Model => {
let mut c = content.flex().flex_col();
if let Some(ref pane) = data.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) = data.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"),
};
grid = grid.child(ProjectBoxFullElement {
id: data.id_project.clone().into(),
name: data.cached_name.clone(),
cwd: data.cached_cwd.clone(),
accent,
status: data.status,
blink_visible: data.shared_blink.as_ref().map(|b| b.visible.clone()),
active_tab: tab_idx,
content: content.into_any_element(),
});
}
main_row = main_row.child(grid);
root = root.child(main_row);
root = root.child(self.status_bar.clone());
if palette_open {
root = root.child(self.command_palette.clone());
}
root
}
}