feat: add Dioxus and GPUI UI prototypes for framework comparison
Dioxus (ui-dioxus/): 2,169 lines, WebView mode (same wry as Tauri), Catppuccin theme, 12 components, agor-core integration, compiles clean. Evolution path — keeps xterm.js, gradual migration from Tauri. GPUI (ui-gpui/): 2,490 lines, GPU-accelerated rendering, alacritty_terminal for native terminal, 17 files, Catppuccin palette, demo data. Revolution path — pure Rust UI, 120fps target, no WebView. Both are standalone (not in workspace), share agor-core backend. Created for side-by-side comparison to inform framework decision.
This commit is contained in:
parent
90c7315336
commit
f3d2ca78ba
34 changed files with 17467 additions and 0 deletions
278
ui-gpui/src/components/project_box.rs
Normal file
278
ui-gpui/src/components/project_box.rs
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
//! ProjectBox: individual project card with header, tab bar, content area.
|
||||
//!
|
||||
//! Each project gets a card in the grid with:
|
||||
//! - Header: name, status dot, CWD
|
||||
//! - Tab bar: Model / Docs / Files
|
||||
//! - Content area: AgentPane (Model tab), placeholder (Docs/Files)
|
||||
//! - Terminal section in Model tab
|
||||
|
||||
use gpui::*;
|
||||
|
||||
use crate::state::{AgentStatus, Project, ProjectTab};
|
||||
use crate::theme;
|
||||
|
||||
// ── Accent Colors by Index ──────────────────────────────────────────
|
||||
|
||||
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()]
|
||||
}
|
||||
|
||||
// ── Status Indicator ────────────────────────────────────────────────
|
||||
|
||||
fn project_status_dot(status: AgentStatus) -> Div {
|
||||
let color = match status {
|
||||
AgentStatus::Idle => theme::OVERLAY0,
|
||||
AgentStatus::Running => theme::GREEN,
|
||||
AgentStatus::Done => theme::BLUE,
|
||||
AgentStatus::Error => theme::RED,
|
||||
};
|
||||
div()
|
||||
.w(px(8.0))
|
||||
.h(px(8.0))
|
||||
.rounded(px(4.0))
|
||||
.bg(color)
|
||||
}
|
||||
|
||||
// ── Tab Button ──────────────────────────────────────────────────────
|
||||
|
||||
fn tab_button(label: &str, active: bool, accent: Rgba) -> Div {
|
||||
let fg = if active { accent } else { theme::SUBTEXT0 };
|
||||
|
||||
let mut tab = div()
|
||||
.px(px(10.0))
|
||||
.py(px(4.0))
|
||||
.text_size(px(11.0))
|
||||
.text_color(fg)
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.text_color(theme::TEXT));
|
||||
|
||||
if active {
|
||||
tab = tab.border_b_2().border_color(accent);
|
||||
}
|
||||
|
||||
tab.child(label.to_string())
|
||||
}
|
||||
|
||||
// ── ProjectBox View ─────────────────────────────────────────────────
|
||||
|
||||
pub struct ProjectBox {
|
||||
pub project: Project,
|
||||
/// Entity handle for the embedded AgentPane (Model tab)
|
||||
pub agent_pane: Option<Entity<crate::components::agent_pane::AgentPane>>,
|
||||
/// Entity handle for the embedded terminal (Model tab)
|
||||
pub terminal_view: Option<Entity<crate::terminal::renderer::TerminalView>>,
|
||||
}
|
||||
|
||||
impl ProjectBox {
|
||||
pub fn new(project: Project) -> Self {
|
||||
Self {
|
||||
project,
|
||||
agent_pane: None,
|
||||
terminal_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize sub-views. Must be called after the ProjectBox entity is created.
|
||||
pub fn init_subviews(&mut self, cx: &mut Context<Self>) {
|
||||
// Create agent pane with demo messages
|
||||
let agent_pane = cx.new(|_cx| {
|
||||
crate::components::agent_pane::AgentPane::with_demo_messages()
|
||||
});
|
||||
self.agent_pane = Some(agent_pane);
|
||||
|
||||
// Create terminal view with demo content
|
||||
let terminal_view = cx.new(|_cx| {
|
||||
let mut tv = crate::terminal::renderer::TerminalView::new(120, 10);
|
||||
tv.feed_demo();
|
||||
tv
|
||||
});
|
||||
self.terminal_view = Some(terminal_view);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProjectBox {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let accent = accent_color(self.project.accent_index);
|
||||
let name = self.project.name.clone();
|
||||
let cwd = self.project.cwd.clone();
|
||||
let status = self.project.agent.status;
|
||||
let active_tab = self.project.active_tab;
|
||||
|
||||
// Build content area based on active tab
|
||||
let content = match active_tab {
|
||||
ProjectTab::Model => {
|
||||
let mut model_content = div()
|
||||
.id(SharedString::from(format!("model-content-{}", self.project.id)))
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col();
|
||||
|
||||
// Agent pane (upper portion)
|
||||
if let Some(ref pane) = self.agent_pane {
|
||||
model_content = model_content.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.child(pane.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
// Resize handle
|
||||
model_content = model_content.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("resize-{}", self.project.id)))
|
||||
.w_full()
|
||||
.h(px(4.0))
|
||||
.bg(theme::SURFACE0)
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(theme::SURFACE1)),
|
||||
);
|
||||
|
||||
// Terminal view
|
||||
if let Some(ref term) = self.terminal_view {
|
||||
model_content = model_content.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h(px(180.0))
|
||||
.child(term.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
model_content
|
||||
}
|
||||
ProjectTab::Docs => {
|
||||
div()
|
||||
.id(SharedString::from(format!("docs-content-{}", self.project.id)))
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_size(px(14.0))
|
||||
.text_color(theme::OVERLAY0)
|
||||
.child("Documentation viewer — renders project markdown files")
|
||||
}
|
||||
ProjectTab::Files => {
|
||||
div()
|
||||
.id(SharedString::from(format!("files-content-{}", self.project.id)))
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.p(px(12.0))
|
||||
.gap(px(4.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::SUBTEXT0)
|
||||
.child("src/"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pl(px(16.0))
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::TEXT)
|
||||
.child("main.rs"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pl(px(16.0))
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::TEXT)
|
||||
.child("lib.rs"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(theme::SUBTEXT0)
|
||||
.child("Cargo.toml"),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
.id(SharedString::from(format!("project-{}", self.project.id)))
|
||||
.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 ──────────────────────────────────────────
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h(px(36.0))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.px(px(12.0))
|
||||
.gap(px(8.0))
|
||||
.bg(theme::MANTLE)
|
||||
.border_b_1()
|
||||
.border_color(theme::SURFACE0)
|
||||
// Accent stripe on left
|
||||
.child(
|
||||
div()
|
||||
.w(px(3.0))
|
||||
.h(px(20.0))
|
||||
.rounded(px(2.0))
|
||||
.bg(accent),
|
||||
)
|
||||
// Status dot
|
||||
.child(project_status_dot(status))
|
||||
// Project name
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.text_color(theme::TEXT)
|
||||
.child(name),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
// CWD (ellipsized)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme::OVERLAY0)
|
||||
.max_w(px(200.0))
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.child(cwd),
|
||||
),
|
||||
)
|
||||
// ── Tab Bar ─────────────────────────────────────────
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h(px(32.0))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.px(px(8.0))
|
||||
.gap(px(2.0))
|
||||
.bg(theme::MANTLE)
|
||||
.border_b_1()
|
||||
.border_color(theme::SURFACE0)
|
||||
.child(tab_button("Model", active_tab == ProjectTab::Model, accent))
|
||||
.child(tab_button("Docs", active_tab == ProjectTab::Docs, accent))
|
||||
.child(tab_button("Files", active_tab == ProjectTab::Files, accent)),
|
||||
)
|
||||
// ── Content Area ────────────────────────────────────
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue