//! 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, sidebar: Entity, settings_panel: Entity, project_boxes: Vec, status_bar: Entity, command_palette: Entity, } impl Workspace { pub fn new(app_state: Entity, cx: &mut Context) -> 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 = 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, 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) -> 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 } }