From 98de2a455f59d6f6e3aaace784955c954b1cac66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 7 Oct 2026 12:53:04 -0400 Subject: [PATCH] =?UTF-8?q?Kit=20=C2=B7=20K03:=20init=20checks=20every=20b?= =?UTF-8?q?undled=20file=20through=20the=20app's=20source;=20Assets::befor?= =?UTF-8?q?e=20joins=20another=20source?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + frontend/src/docs/getting-started.md | 2 +- frontend/src/docs/icons.md | 4 +- frontend/src/docs/installation.md | 3 +- src/assets.rs | 104 ++++++++++++++++++++++++++- src/lib.rs | 17 ++--- tasks/kit.md | 3 +- 7 files changed, 119 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8460707..da0777c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -149,6 +149,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark. - Pixels a host draws at a box's size go through `rendering::RenderSurface`: it asks for one BGRA frame at the box's device pixels in prepaint, draws again only for a new size or revision, and frees the old frame and, on leave, the last one. The revision covers all the frame reads, theme colors too. - Tests type a shortcut as it is bound: a `secondary-` binding is Command on macOS and Control elsewhere, so its test types `secondary-`; `cmd-` is the platform key on every OS and reaches only handlers that read that modifier (`lists::SelectableList`). A Tab count runs through `forms::EyeDropper` only on macOS, where it is enabled. - `img()` keeps loading state, and moves an animated picture's frames, only with an id; the frames hold still while its window is inactive or gpui's own `reduce_motion` is on, which the theme's reduced motion does not set. Content masks are rectangles, but `img()` draws only the part of its picture inside its box and rounds that part with its own radius, so a `Cover` crop keeps its corners. +- An app has one asset source: `Assets`, or `Assets::before(other)` when it has files of its own or runs another library such as GPUI Kit; Ely's paths load from Ely, the rest from `other`. `init` loads every file Ely bundles through the app's source and fails naming the first it cannot, since gpui's svg renderer reads IBM Plex there too. - gpui reads a `&str` picture source with a scheme as a web address, and any other as an embedded asset. A file goes through `Path` (`documents::source` in the crate, `ui::picture` in the gallery). - Taffy sizes a flex column by the height a ratio box has at its set width, before its cap, so chat's `shaped` pictures sit in a block inside such a column (`chat::ImageMessage`, `VideoMessage`, `ImageGrid`). - Taffy 0.13 collapses a child's vertical margin through a block parent with no padding or border on that side, and its size cache keeps only that block's size, so a box that measures it again leaves the margin out. A component moves a box with a relative offset, or with padding where what follows moves too, not with such a margin: `layout::Drawer` pulls and snaps its panel, and a fullscreen `overlays::Dialog` slides its card, with `top`; `motion::Stagger`, `Transition`, a centered Dialog and the palette's card rise with padding, so the box a scrim centers grows with them; the gallery's inspector sample is its probe's own child. A margin that stops at its parent is safe: a flex or grid item's own, or a child's of a flex item or of a box with padding or a border on that side. A flex wrapper stops the collapse but gives its items a definite height, so percent heights inside start to resolve (the Drawer's `max_h(relative(0.85))`). diff --git a/frontend/src/docs/getting-started.md b/frontend/src/docs/getting-started.md index 058a3ee..44fa431 100644 --- a/frontend/src/docs/getting-started.md +++ b/frontend/src/docs/getting-started.md @@ -6,7 +6,7 @@ An Ely app is a GPUI app with four more parts: Ely's assets, one call to `init`, | Part | Why | | --- | --- | -| `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` returns an error without them. | +| `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` returns an error when the source cannot load one of them. | | `ely_gpui_component::init(cx)?` | Call once, before any window. It returns an error if the asset source fails. It registers the fonts, sets the theme, and binds Tab, Shift-Tab and the keys of text fields, the code editor, documents and, natively, the terminal. | | A root `FocusScope` | `FocusScope::new(&handle).root()` owns Tab for the window and takes focus back when the focused element leaves the tree. Focus its handle when the window opens. | | `cx.theme()` | Colors, sizes and radii come from the theme. Components read it; an app's own boxes read it too. | diff --git a/frontend/src/docs/icons.md b/frontend/src/docs/icons.md index c91f785..294cf69 100644 --- a/frontend/src/docs/icons.md +++ b/frontend/src/docs/icons.md @@ -1,6 +1,6 @@ # Icons, fonts and assets -`Assets` embeds Ely's icons and fonts in the binary. Pass it to `with_assets`; `init` registers the fonts and checks that `icons/check.svg` loads. +`Assets` embeds Ely's icons and fonts in the binary. Pass it to `with_assets`, or `Assets::before(other)` to serve Ely's files first and every other path from another source; `init` checks that each of Ely's files loads through the app's source, then registers the fonts. ## Icons @@ -42,7 +42,7 @@ A name rule beats an extension rule, and both beat Ely's map. ## An app's own assets -GPUI takes one asset source. An app with files of its own writes an `AssetSource` that answers its own paths and hands every other path to `Assets`. `init` returns an error unless `icons/check.svg` loads through it. +GPUI takes one asset source. An app with files of its own, or another library's, passes `Assets::before(theirs)`: Ely's paths load from Ely, every other path from `theirs`, whose errors pass through. `init` returns an error naming the first of Ely's files the source cannot load. Use `Icon::from_path("app-icons/mark.svg")` to draw one of those SVG assets with Ely's themed sizing and color. It also accepts an owned `String` or `SharedString`. Paths are asset keys resolved by `AssetSource`; a path it lacks logs an error on first draw and shows a red broken-picture icon. SVGs render as single-color masks; `IconName` and `IconPicker` continue to list the bundled icons. diff --git a/frontend/src/docs/installation.md b/frontend/src/docs/installation.md index bbf6191..492e913 100644 --- a/frontend/src/docs/installation.md +++ b/frontend/src/docs/installation.md @@ -41,8 +41,7 @@ A window opens with a counter and a Press button. From a clone of the repository | Symptom | Cause and fix | | --- | --- | -| `init` error: `` ely: pass `ely_gpui_component::Assets` to `Application::with_assets` `` | `init` found no Ely assets. Start the app with `gpui_platform::application().with_assets(Assets)`. | -| `init` error: `ely: asset source failed` | The app's asset source returned an error. The error carries it. | +| `init` error: `ely: the asset source lacks N of Ely's files, first : ` | The app's source cannot load every file Ely bundles. Pass `Assets`, or `Assets::before(yours)` when the app has a source of its own. | | `init` error: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. | | Windows open with no text on macOS | `gpui_platform` lacks the `font-kit` feature. | | `mismatched types`, with the note ``there are multiple different versions of crate `gpui` in the dependency graph`` | `gpui` names another snapshot or a git source. Use `gpui-pre` at `=0.3.8`. | diff --git a/src/assets.rs b/src/assets.rs index 5c075ef..8c70d69 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -4,7 +4,7 @@ use anyhow::Context as _; use gpui::{App, AssetSource, Result, SharedString}; use rust_embed::RustEmbed; -/// Icons and fonts. Pass to `Application::with_assets`. +/// Icons and fonts. Pass to `Application::with_assets`, alone or through `Assets::before`. #[derive(RustEmbed)] #[folder = "assets"] pub struct Assets; @@ -34,3 +34,105 @@ pub(crate) fn load_fonts(cx: &App) -> Result<()> { log::info!("assets: registering {} fonts", fonts.len()); cx.text_system().add_fonts(fonts) } + +/// Ely's files first, then another source's: one source for an app that runs both. +pub struct Before(T); + +impl Assets { + /// Serves Ely's files, and every other path from `other`, such as GPUI Kit's `Assets`. + pub fn before(other: T) -> Before { + Before(other) + } +} + +impl AssetSource for Before { + fn load(&self, path: &str) -> Result>> { + match Assets.load(path)? { + Some(data) => Ok(Some(data)), + None => self.0.load(path), + } + } + + fn list(&self, path: &str) -> Result> { + let mut names = Assets.list(path)?; + names.extend(self.0.list(path)?); + names.sort(); + names.dedup(); + Ok(names) + } +} + +/// Ely's files the source cannot load, each with the reason. +pub(crate) fn missing(source: &dyn AssetSource) -> Vec { + Assets::iter() + .filter_map(|path| match source.load(&path) { + Ok(Some(_)) => None, + Ok(None) => Some(format!("{path}: not found")), + Err(error) => Some(format!("{path}: {error}")), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use gpui::{AssetSource, Result, SharedString}; + + use super::{Assets, missing}; + + const PLEX: &str = "fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf"; + + /// Icons alone, and an error for any other path, as GPUI Kit's source answers. + struct IconsOnly; + + impl AssetSource for IconsOnly { + fn load(&self, path: &str) -> Result>> { + if path == "icons/kit-only.svg" { + return Ok(Some(Cow::Borrowed(b""))); + } + match Assets.load(path)? { + Some(data) if path.starts_with("icons/") => Ok(Some(data)), + _ => Err(anyhow::anyhow!("could not find asset at path {path}")), + } + } + + fn list(&self, _: &str) -> Result> { + Ok(vec!["icons/check.svg".into(), "icons/kit-only.svg".into()]) + } + } + + #[test] + fn ely_s_own_source_holds_every_file() { + assert_eq!(missing(&Assets), Vec::::new()); + } + + #[test] + fn an_icons_only_source_lacks_ely_s_fonts() { + let missing = missing(&IconsOnly); + assert!( + missing.iter().any(|line| line.starts_with(PLEX)), + "{missing:?}" + ); + } + + #[test] + fn ely_s_files_come_first_then_the_other_s() { + let both = Assets::before(IconsOnly); + assert_eq!(missing(&both), Vec::::new()); + let plex = both.load(PLEX).unwrap().unwrap(); + assert_eq!(plex, Assets.load(PLEX).unwrap().unwrap()); + assert_eq!( + &*both.load("icons/kit-only.svg").unwrap().unwrap(), + b"" + ); + assert!(both.load("icons/neither.svg").is_err()); + let icons = both.list("icons/").unwrap(); + let checks = icons + .iter() + .filter(|name| *name == "icons/check.svg") + .count(); + assert_eq!(checks, 1); + assert!(icons.iter().any(|name| name == "icons/kit-only.svg")); + } +} diff --git a/src/lib.rs b/src/lib.rs index 63ecb13..33a5d09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,22 +45,23 @@ pub mod theme; pub mod tooling; pub mod typography; -pub use assets::Assets; +pub use assets::{Assets, Before}; use anyhow::Context as _; use gpui::{App, KeyBinding}; -use crate::primitives::{FOCUS_CONTEXT, FocusNext, FocusPrev, IconName}; +use crate::primitives::{FOCUS_CONTEXT, FocusNext, FocusPrev}; /// Loads fonts and the theme, binds Tab and text keys. Call once, first. pub fn init(cx: &mut App) -> anyhow::Result<()> { - match cx.asset_source().load(IconName::Check.path()) { - Ok(Some(_)) => {} - Ok(None) => { - anyhow::bail!("ely: pass `ely_gpui_component::Assets` to `Application::with_assets`") - } - Err(error) => return Err(error.context("ely: asset source failed")), + let missing = assets::missing(cx.asset_source().as_ref()); + if let Some(first) = missing.first() { + anyhow::bail!( + "ely: the asset source lacks {} of Ely's files, first {first}; pass `ely_gpui_component::Assets`, or `Assets::before` your own, to `Application::with_assets`", + missing.len() + ); } + log::info!("assets: the app's source loads all of Ely's files"); assets::load_fonts(cx).context("ely: embedded fonts failed to register")?; setup(cx); Ok(()) diff --git a/tasks/kit.md b/tasks/kit.md index f43cf3f..481df73 100644 --- a/tasks/kit.md +++ b/tasks/kit.md @@ -15,7 +15,7 @@ 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` -- [ ] K03 Assets: `init` checks every embedded file through the app's source; one source serves Ely's files and another's +- [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 @@ -26,3 +26,4 @@ Rules: the work order follows dependencies, since the PRD has no chapter for it. 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.