Kit · K04: compat/kit runs Ely beside the newest GPUI Kit in one window; check.sh holds it to Kit's newest release
This commit is contained in:
1 parent
98de2a455f
commit
99615e1a0c
10 files changed
+352
-6
No files matched your search
@@ -2,3 +2,4 @@
|
||||
node_modules/
|
||||
/frontend/dist/
|
||||
.DS_Store
|
||||
/compat/kit/Cargo.lock
|
||||
@@ -29,7 +29,8 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- In `frontend/`: `ELY_GALLERY_ASSETS=<site>/gallery/assets/ pnpm gallery` builds the gallery into `public/gallery`, `pnpm dev` serves the site, `pnpm build` checks types and writes `dist`, `pnpm deploy` runs `wrangler deploy`, `pnpm showcase` writes the showcase catalog into `src/showcase.json` and `public/showcase` (both ignored): it downloads the commit `showcase.lock` pins, installs that commit's packages and runs its own validator, so a bad catalog fails the build; `dev` and `build` run it first, and `ELY_SHOWCASE=<checkout>` reads a local catalog instead. `pnpm test` runs the iso engine's, the mark's and the showcase list's tests, and `pnpm local` builds the gallery for localhost, builds the site and serves both on http://localhost:4173/ (wasm-bindgen and wasm-opt on PATH, as `scripts/web.sh` says). `node scripts/og.mjs`, with `pnpm preview` up, renders `public/og.png` from the hero and the header's mark. `node scripts/favicon.mjs` writes `public/favicon.svg`, which follows the system's mode, `public/apple-touch-icon.png` and the repo's `logo.png` (2048px, transparent) from the mark, in the colors `styles.css` sets. The site reads `examples/gallery/stories.json` for its chapters, stories and search.
|
||||
- `python3 scripts/stories.py` writes `examples/gallery/stories.json`, every page's sections in drawing order with their components, for the site: the names in a title, and in its `SHOWN` table what a story draws beyond them. It fails on a section opened inside another and on a ticked entry with a home of its own that no story names. `scripts/check.sh` refuses a stale one.
|
||||
- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules and lints the gallery for wasm32.
|
||||
- `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
|
||||
- `cargo test --manifest-path compat/kit/Cargo.toml --target-dir target/kit` runs Ely beside GPUI Kit in one window: Tab through both, each library's dialog over the other's fields, one asset source. `scripts/check.sh` first resolves it afresh and holds `gpui-kit` to the newest release `cargo search` names, so a Kit release that moves its gpui pin fails resolution there, naming `gpui-pre`; Ely's pin then follows. Its lockfile stays out of git.
|
||||
- `rm -rf target/debug/incremental target/kit/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
|
||||
- `scripts/icons.sh <lucide-name>...` adds icons. Then add a line to `IconName` in `src/primitives/icon.rs`.
|
||||
- `python3 scripts/countries.py <ne_110m_admin_0_countries.geojson>` writes the bundled world; `python3 scripts/tiles.py <same file>` writes the gallery's map tiles.
|
||||
|
||||
@@ -38,6 +39,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
|
||||
- `src/<chapter>/`: one folder per chapter of `gpui-components.md`.
|
||||
- `src/theme`: palettes, tokens, `ActiveTheme`. `src/motion`: durations, easings, spring.
|
||||
- `examples/gallery/pages/<chapter>.rs`: one page per chapter, in PRD order.
|
||||
- `compat/kit`: a crate of its own, with its own workspace, that tests Ely beside GPUI Kit.
|
||||
- `TASKS.md` and `tasks/`: progress. Every PRD entry has a line and a tag; `tasks/reviews.md` keeps every review round.
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "ely-kit-compat"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
publish = false
|
||||
description = "Ely and the newest GPUI Kit in one app and one window."
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
ely-gpui-component = { path = "../..", features = ["test-support"] }
|
||||
gpui-kit = { version = ">=0.7.1", features = ["test-support"] }
|
||||
|
||||
[workspace]
|
||||
@@ -0,0 +1,26 @@
|
||||
use ely_gpui_component::Assets;
|
||||
use gpui_kit::AssetSource;
|
||||
|
||||
#[test]
|
||||
fn one_source_serves_both_libraries() {
|
||||
let kit = gpui_kit::assets::Assets;
|
||||
let both = Assets::before(gpui_kit::assets::Assets);
|
||||
let ely = Assets.list("").unwrap();
|
||||
assert!(ely.len() > 300, "Ely bundles {} files", ely.len());
|
||||
for path in &ely {
|
||||
let served = both.load(path).unwrap().expect("Ely's file loads");
|
||||
assert_eq!(
|
||||
served,
|
||||
Assets.load(path).unwrap().unwrap(),
|
||||
"{path} is Ely's"
|
||||
);
|
||||
}
|
||||
let icons = kit.list("icons/").unwrap();
|
||||
assert!(!icons.is_empty(), "Kit lists its icons");
|
||||
for path in &icons {
|
||||
let served = both.load(path).unwrap().expect("Kit's icon loads");
|
||||
if Assets.load(path).unwrap().is_none() {
|
||||
assert_eq!(served, kit.load(path).unwrap().unwrap(), "{path} is Kit's");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use ely_gpui_component::{buttons::Button, overlays::Dialog, primitives::FocusScope};
|
||||
use gpui_kit::{
|
||||
AppContext, Bounds, Context, Entity, FocusHandle, IntoElement, KeyUpEvent, Keystroke,
|
||||
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, WindowBounds,
|
||||
WindowOptions,
|
||||
component::{
|
||||
WindowExt,
|
||||
input::{Editor, EditorState, Input, InputState},
|
||||
},
|
||||
div, point,
|
||||
prelude::*,
|
||||
px, size,
|
||||
};
|
||||
|
||||
pub const CODE: &str = "fn main() {}";
|
||||
|
||||
/// Ely's root scope inside Kit's `Root`.
|
||||
pub struct Desk {
|
||||
pub root: FocusHandle,
|
||||
pub open: FocusHandle,
|
||||
pub line: Entity<InputState>,
|
||||
pub code: Entity<EditorState>,
|
||||
pub ely_dialog: bool,
|
||||
}
|
||||
|
||||
impl Render for Desk {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
FocusScope::new(&self.root)
|
||||
.root()
|
||||
.size_full()
|
||||
.p_4()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
div().debug_selector(|| "ely-open".into()).child(
|
||||
Button::new("ely-open", "Open")
|
||||
.focus_handle(&self.open)
|
||||
.on_click(|_, window, cx| {
|
||||
window.open_dialog(cx, |dialog, _, _| dialog.title("Kit dialog"));
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(Input::new(&self.line))
|
||||
.child(div().h(px(120.)).child(Editor::new(&self.code).size_full()))
|
||||
.when(self.ely_dialog, |desk| {
|
||||
desk.child(
|
||||
Dialog::new("ely-dialog", "Ely dialog", move |_, cx| {
|
||||
view.update(cx, |desk, cx| {
|
||||
desk.ely_dialog = false;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.action(|_| Button::new("ely-first", "First"))
|
||||
.action(|_| Button::new("ely-second", "Second")),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts both libraries in order; Kit opens the window.
|
||||
pub fn open(cx: &mut TestAppContext, ely_last: bool) -> (Entity<Desk>, VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
if ely_last {
|
||||
gpui_kit::init(cx);
|
||||
ely_gpui_component::init_for_tests(cx);
|
||||
} else {
|
||||
ely_gpui_component::init_for_tests(cx);
|
||||
gpui_kit::init(cx);
|
||||
}
|
||||
});
|
||||
let (window, desk) = cx.update(|cx| {
|
||||
let options = WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(Bounds {
|
||||
origin: point(px(0.), px(0.)),
|
||||
size: size(px(800.), px(600.)),
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
gpui_kit::open_window(options, cx, |window, cx| {
|
||||
cx.new(|cx| Desk {
|
||||
root: cx.focus_handle(),
|
||||
open: cx.focus_handle().tab_stop(true),
|
||||
line: cx.new(|cx| InputState::new(window, cx)),
|
||||
code: cx.new(|cx| {
|
||||
EditorState::new(window, cx)
|
||||
.language("rust")
|
||||
.default_value(CODE)
|
||||
}),
|
||||
ely_dialog: false,
|
||||
})
|
||||
})
|
||||
.expect("Kit opens the window")
|
||||
});
|
||||
let cx = VisualTestContext::from_window(window, cx);
|
||||
cx.run_until_parked();
|
||||
(desk, cx)
|
||||
}
|
||||
|
||||
/// Whether the handle holds focus.
|
||||
pub fn focused(cx: &mut VisualTestContext, handle: &FocusHandle) -> bool {
|
||||
cx.update(|window, _| handle.is_focused(window))
|
||||
}
|
||||
|
||||
/// Key down and up; gpui presses on the release.
|
||||
pub fn press(cx: &mut VisualTestContext, key: &str) {
|
||||
cx.simulate_keystrokes(key);
|
||||
cx.simulate_event(KeyUpEvent {
|
||||
keystroke: Keystroke::parse(key).expect("a key"),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui_kit::{
|
||||
FocusHandle, Focusable, Modifiers, TestAppContext, VisualTestContext, test::TestWindowExt,
|
||||
};
|
||||
|
||||
use crate::desk::{focused, open, press};
|
||||
|
||||
/// Draws until `done` holds; fails after a second.
|
||||
fn settle(cx: &mut VisualTestContext, done: impl Fn(&mut VisualTestContext) -> bool) {
|
||||
for _ in 0..50 {
|
||||
cx.run_until_parked();
|
||||
cx.update(|window, cx| window.render_frame(cx));
|
||||
if done(cx) {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
panic!("the window did not settle");
|
||||
}
|
||||
|
||||
fn kit_dialog_open(cx: &mut VisualTestContext) -> bool {
|
||||
cx.update(|window, _| window.try_find("dialog").is_some())
|
||||
}
|
||||
|
||||
/// Opens Kit's dialog, tabs inside it, closes it.
|
||||
fn kit_dialog_round_trip(
|
||||
cx: &mut TestAppContext,
|
||||
open_with: impl FnOnce(&mut VisualTestContext, &Handles),
|
||||
back_to: impl Fn(&Handles) -> FocusHandle,
|
||||
) {
|
||||
let (desk, mut cx) = open(cx, true);
|
||||
let handles = cx.update(|_, cx| {
|
||||
let desk = desk.read(cx);
|
||||
Handles {
|
||||
button: desk.open.clone(),
|
||||
line: desk.line.read(cx).focus_handle(cx),
|
||||
code: desk.code.read(cx).focus_handle(cx),
|
||||
}
|
||||
});
|
||||
open_with(&mut cx, &handles);
|
||||
settle(&mut cx, kit_dialog_open);
|
||||
for _ in 0..3 {
|
||||
cx.simulate_keystrokes("tab");
|
||||
for handle in [&handles.button, &handles.line, &handles.code] {
|
||||
assert!(!focused(&mut cx, handle), "Tab stays in Kit's dialog");
|
||||
}
|
||||
}
|
||||
cx.simulate_keystrokes("escape");
|
||||
settle(&mut cx, |cx| !kit_dialog_open(cx));
|
||||
assert!(
|
||||
focused(&mut cx, &back_to(&handles)),
|
||||
"focus returns where it was"
|
||||
);
|
||||
}
|
||||
|
||||
struct Handles {
|
||||
button: FocusHandle,
|
||||
line: FocusHandle,
|
||||
code: FocusHandle,
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
fn enter_on_an_ely_button_opens_a_kit_dialog_that_hands_focus_back(cx: &mut TestAppContext) {
|
||||
kit_dialog_round_trip(
|
||||
cx,
|
||||
|cx, handles| {
|
||||
cx.update(|window, cx| window.focus(&handles.button, cx));
|
||||
press(cx, "enter");
|
||||
},
|
||||
|handles| handles.button.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
fn a_press_on_an_ely_button_leaves_focus_for_the_kit_dialog_to_return(cx: &mut TestAppContext) {
|
||||
kit_dialog_round_trip(
|
||||
cx,
|
||||
|cx, handles| {
|
||||
cx.update(|window, cx| window.focus(&handles.line, cx));
|
||||
let bounds = cx.debug_bounds("ely-open").expect("Ely's button drew");
|
||||
cx.simulate_click(bounds.center(), Modifiers::none());
|
||||
},
|
||||
|handles| handles.line.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
fn an_ely_dialog_over_kit_fields_traps_tab_and_hands_focus_back(cx: &mut TestAppContext) {
|
||||
let (desk, mut cx) = open(cx, true);
|
||||
let (button, line, code) = cx.update(|_, cx| {
|
||||
let desk = desk.read(cx);
|
||||
(
|
||||
desk.open.clone(),
|
||||
desk.line.read(cx).focus_handle(cx),
|
||||
desk.code.read(cx).focus_handle(cx),
|
||||
)
|
||||
});
|
||||
cx.update(|window, cx| window.focus(&line, cx));
|
||||
desk.update(&mut cx, |desk, cx| {
|
||||
desk.ely_dialog = true;
|
||||
cx.notify();
|
||||
});
|
||||
settle(&mut cx, |cx| !focused(cx, &line));
|
||||
for _ in 0..3 {
|
||||
cx.simulate_keystrokes("tab");
|
||||
for handle in [&button, &line, &code] {
|
||||
assert!(!focused(&mut cx, handle), "Tab stays in Ely's dialog");
|
||||
}
|
||||
}
|
||||
cx.simulate_keystrokes("escape");
|
||||
settle(&mut cx, |cx| focused(cx, &line));
|
||||
assert!(
|
||||
!desk.read_with(&cx, |desk, _| desk.ely_dialog),
|
||||
"Escape closed it"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use gpui_kit::{Focusable, TestAppContext};
|
||||
|
||||
use crate::desk::{CODE, focused, open};
|
||||
|
||||
fn tab_runs_through_both(cx: &mut TestAppContext, ely_last: bool) {
|
||||
let (desk, mut cx) = open(cx, ely_last);
|
||||
let (open_button, line, code) = cx.update(|_, cx| {
|
||||
let desk = desk.read(cx);
|
||||
(desk.open.clone(), desk.line.clone(), desk.code.clone())
|
||||
});
|
||||
cx.update(|window, cx| window.focus(&open_button, cx));
|
||||
cx.simulate_keystrokes("tab");
|
||||
let line_focus = cx.update(|_, cx| line.read(cx).focus_handle(cx));
|
||||
assert!(
|
||||
focused(&mut cx, &line_focus),
|
||||
"Tab leaves Ely's button for Kit's field"
|
||||
);
|
||||
cx.simulate_keystrokes("tab");
|
||||
let code_focus = cx.update(|_, cx| code.read(cx).focus_handle(cx));
|
||||
assert!(
|
||||
focused(&mut cx, &code_focus),
|
||||
"Tab reaches Kit's code editor"
|
||||
);
|
||||
cx.simulate_keystrokes("tab");
|
||||
assert!(
|
||||
focused(&mut cx, &code_focus),
|
||||
"Tab stays in the code editor"
|
||||
);
|
||||
let text = cx.update(|_, cx| code.read(cx).value().to_string());
|
||||
assert!(
|
||||
text.len() > CODE.len() && text.trim() == CODE,
|
||||
"Tab indents: {text:?}"
|
||||
);
|
||||
cx.simulate_keystrokes("shift-tab");
|
||||
let text = cx.update(|_, cx| code.read(cx).value().to_string());
|
||||
assert_eq!(text, CODE, "Shift-Tab outdents");
|
||||
cx.update(|window, cx| window.focus(&line_focus, cx));
|
||||
cx.simulate_keystrokes("shift-tab");
|
||||
assert!(
|
||||
focused(&mut cx, &open_button),
|
||||
"Shift-Tab returns to Ely's button"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
fn tab_runs_through_both_with_ely_started_last(cx: &mut TestAppContext) {
|
||||
tab_runs_through_both(cx, true);
|
||||
}
|
||||
|
||||
#[gpui_kit::test]
|
||||
fn tab_runs_through_both_with_kit_started_last(cx: &mut TestAppContext) {
|
||||
tab_runs_through_both(cx, false);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//! Ely and GPUI Kit in one app and one window.
|
||||
|
||||
#[cfg(test)]
|
||||
mod assets;
|
||||
#[cfg(test)]
|
||||
mod desk;
|
||||
#[cfg(test)]
|
||||
mod dialogs;
|
||||
#[cfg(test)]
|
||||
mod focus;
|
||||
@@ -4,6 +4,15 @@ set -eu
|
||||
cargo fmt --check
|
||||
cargo clippy --all-targets --features test-support -- -D warnings
|
||||
cargo test --lib --features test-support --quiet
|
||||
# Ely beside the newest GPUI Kit: a release that moves Kit's gpui pin fails to resolve here.
|
||||
kit="--manifest-path compat/kit/Cargo.toml"
|
||||
newest=$(cargo search gpui-kit --limit 1 | sed -n 's/^gpui-kit = "\([^"]*\)".*/\1/p')
|
||||
[ -n "$newest" ] || { echo "crates.io named no gpui-kit release" >&2; exit 1; }
|
||||
cargo generate-lockfile $kit
|
||||
cargo update $kit -p gpui-kit --precise "$newest"
|
||||
cargo fmt $kit --check
|
||||
cargo clippy $kit --all-targets --target-dir target/kit -- -D warnings
|
||||
cargo test $kit --target-dir target/kit --quiet
|
||||
rustup target list --installed | grep -qx wasm32-unknown-unknown || {
|
||||
echo "the web lint needs wasm32: rustup target add wasm32-unknown-unknown" >&2
|
||||
exit 1
|
||||
|
||||
+5
-5
@@ -7,7 +7,7 @@ Started 2026-10-07. Ely and Longbridge's GPUI Kit (`gpui-kit` on crates.io) shar
|
||||
- Ely binds Tab with no context, which gpui ranks at the deepest level. In a Kit code editor inside an Ely scope, Tab ties with Kit's indent, and the later `init` wins.
|
||||
- An app has one asset source. Of Ely's 306 icons, Kit's source holds 290 paths, 289 with the same drawing; `credit-card` differs by one stroke (Lucide 1.48 against 1.43). Ely's `init` checked one icon, so Kit's source alone passed and Ely's other files went missing later.
|
||||
- Kit's `init` installs its inspector in debug builds; Ely's `install_inspector` replaces it when called after.
|
||||
- Kit can run on `gpui-fast` instead; Ely could not follow.
|
||||
- Kit can also run on `gpui-fast`; at the owner's word, Ely follows Kit's default gpui alone.
|
||||
|
||||
Rules: the work order follows dependencies, since the PRD has no chapter for it. Each item passes `scripts/check.sh` and a codex review (`gpt-6.1-sol`, effort `high`) in a herdr split, five rounds at most, then commits and pushes.
|
||||
|
||||
@@ -16,10 +16,9 @@ Rules: the work order follows dependencies, since the PRD has no chapter for it.
|
||||
- [x] K01 gpui from `gpui-pre =0.3.8`, Kit's pin: crate, gallery, web build, docs, CI
|
||||
- [x] K02 Tab and Shift-Tab bound under `FocusScope`'s own context, so a deeper context's Tab wins whatever the order of `init`
|
||||
- [x] K03 Assets: `init` checks every embedded file through the app's source; one source serves Ely's files and another's
|
||||
- [ ] K04 `compat/kit`: a crate on the newest Kit that runs both libraries in one window, in `scripts/check.sh` and CI
|
||||
- [ ] K05 `gpui-fast`: Ely builds and tests on Kit's other engine, or the item records why not
|
||||
- [ ] K06 Docs: README, the site's guide, AGENTS.md
|
||||
- [ ] K07 Acceptance table
|
||||
- [x] K04 `compat/kit`: a crate on the newest Kit that runs both libraries in one window, in `scripts/check.sh` and CI
|
||||
- [ ] K05 Docs: README, the site's guide, AGENTS.md
|
||||
- [ ] K06 Acceptance table
|
||||
|
||||
## Log
|
||||
|
||||
@@ -27,3 +26,4 @@ One line per item: what changed, what stayed and why, review rounds.
|
||||
- K01: gpui, gpui_platform and gpui_web come from `gpui-pre` `=0.3.8`; the lock holds no git source and one copy of each. `gpui-pre` split leak detection from `test-support`, so this crate's `test-support` turns on `gpui/leak-detection`, and a test that leaks on purpose proves it. The check, the wasm check, the web build (20.0 MB), a headless Chromium shot and the site build pass. README and the install guide name the snapshot. Review: 2 rounds.
|
||||
- K02: `FocusScope` sets the key context `ElyFocus`, and `init` binds Tab and Shift-Tab under it. A host's Tab in a deeper context wins whatever the order of `init` (a test binds the host's first, the order that lost: 0 indents before, 1 after); Ely's terminal, editor, traps and the inspector's scope behave as before. Review: 1 round.
|
||||
- K03: `init` loads every file Ely bundles (318, Plex among them) through the app's source and fails naming the first it cannot; an icons-only source like Kit's passed the old one-icon check and fails now. `Assets::before(other)` serves Ely's paths first and the rest from `other`, its errors passing through. Docs and AGENTS.md say so. Review: 1 round.
|
||||
- K04: `compat/kit`, its own workspace, opens Ely's root scope inside Kit's `Root` and tests Tab through both libraries in each `init` order (red with K02 undone), Kit's dialog from an Ely button by key and by pointer, Ely's dialog over Kit's fields, and one asset source for both. `check.sh` resolves it afresh and holds `gpui-kit` to the newest release, so a Kit release on another gpui fails resolution naming `gpui-pre` (shown with Kit 0.7.0). Review: 2 rounds.
|
||||
Reference in new issue
Block a user