This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# Native UI spikes
|
||||
|
||||
These disposable applications render the same fixture through GPUI and Floem.
|
||||
They are a decision instrument, not product code. The root workspace excludes
|
||||
this nested workspace so normal Lumbridge CI does not download or compile both
|
||||
UI frameworks.
|
||||
|
||||
The GPUI spike uses published `gpui 0.2.2`. The Floem spike pins upstream commit
|
||||
`778bb5f2aa08429e579ee2e6ac97e84fbf18b618`; the crates.io `floem 0.2.0` package
|
||||
lags the current API substantially enough that comparing it to current GPUI
|
||||
would not be representative.
|
||||
|
||||
Both spikes must preserve the same information architecture:
|
||||
|
||||
- workspace/sidebar and remote host state;
|
||||
- two rows of three busy surfaces;
|
||||
- local and remote terminal/agent panes;
|
||||
- native Markdown editor/preview and browser placeholders;
|
||||
- connection, harness, usage, and burn context in the footer.
|
||||
|
||||
Build independently:
|
||||
|
||||
```bash
|
||||
cargo build --release --manifest-path spikes/gpui-shell/Cargo.toml
|
||||
cargo build --release --manifest-path spikes/floem-shell/Cargo.toml
|
||||
```
|
||||
|
||||
Each candidate is an independent Cargo workspace. GPUI pins `taffy 0.9.0`
|
||||
while current Floem requires `taffy 0.9.2`; putting them in one comparison
|
||||
workspace creates an artificial resolver conflict and would let one candidate's
|
||||
dependency decisions distort the other candidate's build.
|
||||
|
||||
The comparison records release build time, binary size, startup, idle RSS,
|
||||
six-pane streaming frame time, key-to-present latency, accessibility/IME,
|
||||
window behavior, browser-child integration, packaging, dependency count, and
|
||||
license closure on macOS, Ubuntu, and Omarchy. A build is not adoption: GPUI's
|
||||
complete dependency-license closure remains a hard gate.
|
||||
Generated
+4680
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "lumbridge-spike-floem"
|
||||
description = "Floem implementation of the Lumbridge native shell spike"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
floem = { git = "https://github.com/lapce/floem", rev = "778bb5f2aa08429e579ee2e6ac97e84fbf18b618", default-features = false, features = ["vger"] }
|
||||
lumbridge-spike-model = { path = "../ui-shell-model" }
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,163 @@
|
||||
use floem::{Application, kurbo::Size, peniko::Color, prelude::*, window::WindowConfig};
|
||||
use lumbridge_spike_model::{
|
||||
FOOTER_CENTER, FOOTER_LEFT, FOOTER_RIGHT, PANES, PaneFixture, WORKSPACES,
|
||||
};
|
||||
|
||||
const BG: Color = Color::from_rgb8(12, 14, 19);
|
||||
const PANEL: Color = Color::from_rgb8(18, 23, 34);
|
||||
const PANEL_ALT: Color = Color::from_rgb8(23, 29, 41);
|
||||
const BORDER: Color = Color::from_rgb8(41, 50, 68);
|
||||
const TEXT: Color = Color::from_rgb8(217, 226, 242);
|
||||
const MUTED: Color = Color::from_rgb8(127, 139, 163);
|
||||
const ACCENT: Color = Color::from_rgb8(119, 189, 251);
|
||||
|
||||
fn pane_card(pane: PaneFixture) -> impl IntoView {
|
||||
let header = Stack::horizontal((
|
||||
Label::new(pane.title).style(|s| s.flex_grow(1.0).color(TEXT)),
|
||||
Label::new(pane.badge).style(|s| s.font_size(11.0).color(ACCENT)),
|
||||
))
|
||||
.style(|s| {
|
||||
s.items_center()
|
||||
.height(34.0)
|
||||
.padding_horiz(10.0)
|
||||
.background(PANEL_ALT)
|
||||
.border_bottom(1.0)
|
||||
.border_color(BORDER)
|
||||
});
|
||||
|
||||
let lines =
|
||||
Stack::vertical(pane.lines.map(|line| {
|
||||
Label::new(line).style(|s| s.font_size(12.0).color(TEXT).margin_bottom(5.0))
|
||||
}));
|
||||
|
||||
Stack::vertical((
|
||||
header,
|
||||
Label::new(pane.target).style(|s| s.font_size(11.0).color(MUTED).padding(10.0)),
|
||||
lines.style(|s| s.padding_horiz(10.0).padding_bottom(10.0)),
|
||||
))
|
||||
.style(|s| {
|
||||
s.flex_basis(0)
|
||||
.flex_grow(1.0)
|
||||
.min_width(0.0)
|
||||
.min_height(0.0)
|
||||
.margin(4.0)
|
||||
.background(PANEL)
|
||||
.border(1.0)
|
||||
.border_color(BORDER)
|
||||
.border_radius(6.0)
|
||||
})
|
||||
}
|
||||
|
||||
fn app_view() -> impl IntoView {
|
||||
let sidebar_items = Stack::vertical(WORKSPACES.map(|name| {
|
||||
Label::new(name).style(move |s| {
|
||||
s.width_full()
|
||||
.padding_vert(8.0)
|
||||
.padding_horiz(12.0)
|
||||
.margin_bottom(3.0)
|
||||
.color(if name == "Lumbridge Code" {
|
||||
TEXT
|
||||
} else {
|
||||
MUTED
|
||||
})
|
||||
.apply_if(name == "Lumbridge Code", |s| {
|
||||
s.background(PANEL_ALT).border_radius(6.0)
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
||||
let sidebar = Stack::vertical((
|
||||
Label::new("WORKSPACES").style(|s| s.font_size(11.0).color(MUTED).padding(12.0)),
|
||||
sidebar_items.style(|s| s.padding_horiz(8.0)),
|
||||
Label::new("HOSTS").style(|s| {
|
||||
s.font_size(11.0)
|
||||
.color(MUTED)
|
||||
.padding(12.0)
|
||||
.margin_top(12.0)
|
||||
}),
|
||||
Label::new("● metal · connected").style(|s| s.color(ACCENT).padding_horiz(14.0)),
|
||||
Label::new("● amd-server · connected")
|
||||
.style(|s| s.color(MUTED).padding_horiz(14.0).margin_top(8.0)),
|
||||
Label::new("○ spark-1 · sleeping")
|
||||
.style(|s| s.color(MUTED).padding_horiz(14.0).margin_top(8.0)),
|
||||
))
|
||||
.style(|s| {
|
||||
s.width(220.0)
|
||||
.height_full()
|
||||
.flex_shrink(0.0)
|
||||
.background(PANEL)
|
||||
.border_right(1.0)
|
||||
.border_color(BORDER)
|
||||
});
|
||||
|
||||
let top_row = Stack::horizontal((
|
||||
pane_card(PANES[0]),
|
||||
pane_card(PANES[1]),
|
||||
pane_card(PANES[2]),
|
||||
))
|
||||
.style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0).width_full());
|
||||
let bottom_row = Stack::horizontal((
|
||||
pane_card(PANES[3]),
|
||||
pane_card(PANES[4]),
|
||||
pane_card(PANES[5]),
|
||||
))
|
||||
.style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0).width_full());
|
||||
let panes = Stack::vertical((top_row, bottom_row)).style(|s| {
|
||||
s.flex_basis(0)
|
||||
.flex_grow(1.0)
|
||||
.min_width(0.0)
|
||||
.height_full()
|
||||
.padding(4.0)
|
||||
});
|
||||
|
||||
let header = Stack::horizontal((
|
||||
Label::new("Lumbridge").style(|s| s.font_size(18.0).color(TEXT).flex_grow(1.0)),
|
||||
Label::new("UI spike · Floem").style(|s| s.color(MUTED).flex_grow(1.0)),
|
||||
Label::new("⌘K Command Palette").style(|s| s.color(ACCENT)),
|
||||
))
|
||||
.style(|s| {
|
||||
s.height(46.0)
|
||||
.padding_horiz(14.0)
|
||||
.items_center()
|
||||
.background(PANEL_ALT)
|
||||
.border_bottom(1.0)
|
||||
.border_color(BORDER)
|
||||
});
|
||||
|
||||
let body = Stack::horizontal((sidebar, panes))
|
||||
.style(|s| s.flex_basis(0).flex_grow(1.0).min_height(0.0));
|
||||
|
||||
let footer = Stack::horizontal((
|
||||
Label::new(FOOTER_LEFT).style(|s| s.flex_grow(1.0)),
|
||||
Label::new(FOOTER_CENTER).style(|s| s.flex_grow(1.0)),
|
||||
Label::new(FOOTER_RIGHT),
|
||||
))
|
||||
.style(|s| {
|
||||
s.height(30.0)
|
||||
.padding_horiz(10.0)
|
||||
.items_center()
|
||||
.font_size(11.0)
|
||||
.color(MUTED)
|
||||
.background(PANEL_ALT)
|
||||
.border_top(1.0)
|
||||
.border_color(BORDER)
|
||||
});
|
||||
|
||||
Stack::vertical((header, body, footer))
|
||||
.style(|s| s.width_full().height_full().background(BG))
|
||||
.window_title(|| "Lumbridge · Floem spike".to_owned())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new()
|
||||
.window(
|
||||
|_| app_view(),
|
||||
Some(
|
||||
WindowConfig::default()
|
||||
.size(Size::new(1280.0, 800.0))
|
||||
.min_size(Size::new(900.0, 600.0))
|
||||
.title("Lumbridge · Floem spike"),
|
||||
),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
Generated
+7195
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "lumbridge-spike-gpui"
|
||||
description = "GPUI implementation of the Lumbridge native shell spike"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
gpui = "0.2.2"
|
||||
lumbridge-spike-model = { path = "../ui-shell-model" }
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,192 @@
|
||||
use gpui::{
|
||||
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
|
||||
rgb, size,
|
||||
};
|
||||
use lumbridge_spike_model::{
|
||||
FOOTER_CENTER, FOOTER_LEFT, FOOTER_RIGHT, PANES, PaneFixture, WORKSPACES,
|
||||
};
|
||||
|
||||
const BG: u32 = 0x0c0e13;
|
||||
const PANEL: u32 = 0x121722;
|
||||
const PANEL_ALT: u32 = 0x171d29;
|
||||
const BORDER: u32 = 0x293244;
|
||||
const TEXT: u32 = 0xd9e2f2;
|
||||
const MUTED: u32 = 0x7f8ba3;
|
||||
const ACCENT: u32 = 0x77bdfb;
|
||||
|
||||
struct LumbridgeShell;
|
||||
|
||||
fn pane_card(pane: &PaneFixture) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.min_w_0()
|
||||
.min_h_0()
|
||||
.overflow_hidden()
|
||||
.bg(rgb(PANEL))
|
||||
.border_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.rounded_md()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.px_3()
|
||||
.h(px(34.0))
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.border_b_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.child(pane.title)
|
||||
.child(div().text_xs().text_color(rgb(ACCENT)).child(pane.badge)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(pane.target),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.px_3()
|
||||
.pb_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(TEXT))
|
||||
.children(pane.lines.into_iter()),
|
||||
)
|
||||
}
|
||||
|
||||
impl Render for LumbridgeShell {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let sidebar = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w(px(220.0))
|
||||
.flex_none()
|
||||
.bg(rgb(PANEL))
|
||||
.border_r_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.child(
|
||||
div()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("WORKSPACES"),
|
||||
)
|
||||
.children(WORKSPACES.into_iter().enumerate().map(|(index, name)| {
|
||||
div()
|
||||
.mx_2()
|
||||
.mb_1()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.rounded_md()
|
||||
.when(index == 0, |view| {
|
||||
view.bg(rgb(PANEL_ALT)).text_color(rgb(TEXT))
|
||||
})
|
||||
.when(index != 0, |view| view.text_color(rgb(MUTED)))
|
||||
.child(name)
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.mt_4()
|
||||
.px_4()
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("HOSTS")
|
||||
.child(
|
||||
div()
|
||||
.mt_2()
|
||||
.text_color(rgb(ACCENT))
|
||||
.child("● metal · connected"),
|
||||
)
|
||||
.child(div().mt_2().child("● amd-server · connected"))
|
||||
.child(div().mt_2().child("○ spark-1 · sleeping")),
|
||||
);
|
||||
|
||||
let grid = div()
|
||||
.grid()
|
||||
.grid_cols(3)
|
||||
.grid_rows(2)
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.size_full()
|
||||
.children(PANES.iter().map(pane_card));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.bg(rgb(BG))
|
||||
.text_color(rgb(TEXT))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.h(px(46.0))
|
||||
.flex_none()
|
||||
.px_4()
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.border_b_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.child(div().text_lg().child("Lumbridge"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(MUTED))
|
||||
.child("UI spike · GPUI"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(ACCENT))
|
||||
.child("⌘K Command Palette"),
|
||||
),
|
||||
)
|
||||
.child(div().flex().flex_1().min_h_0().child(sidebar).child(grid))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.h(px(30.0))
|
||||
.flex_none()
|
||||
.px_3()
|
||||
.bg(rgb(PANEL_ALT))
|
||||
.border_t_1()
|
||||
.border_color(rgb(BORDER))
|
||||
.text_xs()
|
||||
.text_color(rgb(MUTED))
|
||||
.child(FOOTER_LEFT)
|
||||
.child(FOOTER_CENTER)
|
||||
.child(FOOTER_RIGHT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
let bounds = Bounds::centered(None, size(px(1280.0), px(800.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
titlebar: Some(gpui::TitlebarOptions {
|
||||
title: Some("Lumbridge · GPUI spike".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|_, cx| cx.new(|_| LumbridgeShell),
|
||||
)
|
||||
.expect("GPUI spike window should open");
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "lumbridge-spike-model"
|
||||
version = "0.0.1"
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "lumbridge-spike-model"
|
||||
description = "Identical fixture data for Lumbridge native UI comparisons"
|
||||
version = "0.0.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
license = "Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,122 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SurfaceKind {
|
||||
Terminal,
|
||||
Markdown,
|
||||
Browser,
|
||||
Review,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PaneFixture {
|
||||
pub title: &'static str,
|
||||
pub badge: &'static str,
|
||||
pub target: &'static str,
|
||||
pub kind: SurfaceKind,
|
||||
pub lines: [&'static str; 4],
|
||||
}
|
||||
|
||||
pub const PANES: [PaneFixture; 6] = [
|
||||
PaneFixture {
|
||||
title: "Codex · runtime",
|
||||
badge: "WORKING",
|
||||
target: "metal · Tailscale SSH",
|
||||
kind: SurfaceKind::Terminal,
|
||||
lines: [
|
||||
"$ cargo nextest run -p lumbridge-runtime",
|
||||
"PASS remote::reconnect_replays_output",
|
||||
"PASS pty::resize_preserves_cursor",
|
||||
"agent is editing 3 files…",
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
title: "Claude Code · UI",
|
||||
badge: "NEEDS INPUT",
|
||||
target: "MacBook Air · local",
|
||||
kind: SurfaceKind::Terminal,
|
||||
lines: [
|
||||
"$ bacon clippy",
|
||||
"finished in 0.42s",
|
||||
"Which split should receive focus?",
|
||||
"[Approve] [Steer] [Cancel]",
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
title: "Pi · docs",
|
||||
badge: "STREAMING",
|
||||
target: "amd-server · OpenSSH",
|
||||
kind: SurfaceKind::Terminal,
|
||||
lines: [
|
||||
"$ cargo watch --why",
|
||||
"ACP session resumed at event 1842",
|
||||
"writing remote-session.md",
|
||||
"▌",
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
title: "Architecture.md",
|
||||
badge: "MARKDOWN",
|
||||
target: "lumbridge-code · worktree",
|
||||
kind: SurfaceKind::Markdown,
|
||||
lines: [
|
||||
"## Remote session path",
|
||||
"The destination runtime owns the PTY.",
|
||||
"The laptop may disconnect and reattach.",
|
||||
"SQLite remains local to each machine.",
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
title: "Preview · ACP docs",
|
||||
badge: "BROWSER",
|
||||
target: "isolated system web engine",
|
||||
kind: SurfaceKind::Browser,
|
||||
lines: [
|
||||
"https://agentclientprotocol.com",
|
||||
"Content process: sandboxed",
|
||||
"Runtime bridge: no privileged access",
|
||||
"Open in external browser ↗",
|
||||
],
|
||||
},
|
||||
PaneFixture {
|
||||
title: "Changes · lumbridge-runtime",
|
||||
badge: "REVIEW",
|
||||
target: "metal · worktree remote-runtime",
|
||||
kind: SurfaceKind::Review,
|
||||
lines: [
|
||||
"+ 184 remote transport",
|
||||
"+ 96 SQLite migrations",
|
||||
"− 12 obsolete scaffold",
|
||||
"6 files · tests passing",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
pub const WORKSPACES: [&str; 5] = [
|
||||
"Lumbridge Code",
|
||||
"Runtime / metal",
|
||||
"UI spikes",
|
||||
"ACP adapters",
|
||||
"Usage telemetry",
|
||||
];
|
||||
|
||||
pub const FOOTER_LEFT: &str = "6 panes · 3 remote · 1 needs input";
|
||||
pub const FOOTER_CENTER: &str = "Codex · ChatGPT subscription · 62% window remaining";
|
||||
pub const FOOTER_RIGHT: &str = "burn 8.4%/hr · resets in 2h 14m";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{PANES, SurfaceKind};
|
||||
|
||||
#[test]
|
||||
fn comparison_fixture_has_six_mixed_surfaces() {
|
||||
assert_eq!(PANES.len(), 6);
|
||||
assert!(PANES.iter().any(|pane| pane.kind == SurfaceKind::Markdown));
|
||||
assert!(PANES.iter().any(|pane| pane.kind == SurfaceKind::Browser));
|
||||
assert_eq!(
|
||||
PANES
|
||||
.iter()
|
||||
.filter(|pane| pane.kind == SurfaceKind::Terminal)
|
||||
.count(),
|
||||
3
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user