Panics · P01 Primitives: init returns an error, a missing app icon logs and shows a broken picture

This commit is contained in:
ZacharyZhang-NY committed 2026-10-06 15:25:09 -04:00
1 parent 52d3d790b1
commit e5166e9e26
20 files changed
+159 -87

No files matched your search

+2 -2
View File
@@ -22,13 +22,13 @@ use gpui::App;
fn main() {
gpui_platform::application().with_assets(Assets).run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
Theme::set_mode(Mode::Dark, cx);
});
}
```
`init` registers the fonts and the theme. It panics if `Assets` is not wired into the application.
`init` registers the fonts and the theme. It returns an error if `Assets` is not wired into the application or the asset source fails.
## Gallery
+1 -1
View File
@@ -54,7 +54,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let root = cx.focus_handle();
+1 -1
View File
@@ -41,7 +41,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
+1 -1
View File
@@ -53,7 +53,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.set_global(
I18n::new("en-US")
.catalog(
+1 -1
View File
@@ -87,7 +87,7 @@ fn main() {
gpui_platform::application()
.with_assets(AppAssets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
IconTheme::default()
.name("Cargo.toml", IconName::Package)
.apply(cx);
+1 -1
View File
@@ -104,7 +104,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
+1 -1
View File
@@ -78,7 +78,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
+1 -1
View File
@@ -56,7 +56,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
+1 -1
View File
@@ -45,7 +45,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
Theme::set_mode_now(Mode::Dark, cx);
let bounds = Bounds::centered(None, size(px(640.0), px(400.0)), cx);
let options = WindowOptions {
+1 -1
View File
@@ -100,7 +100,7 @@ fn main() {
gpui_platform::application()
.with_assets(Assets)
.run(|cx: &mut App| {
ely_gpui_component::init(cx);
ely_gpui_component::init(cx).expect("Ely failed to start");
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| {
let focus = cx.focus_handle();
+13 -6
View File
@@ -41,7 +41,8 @@ mod web;
use std::path::PathBuf;
#[cfg(not(target_family = "wasm"))]
use anyhow::{Context as _, Result, bail};
use anyhow::bail;
use anyhow::{Context as _, Result};
#[cfg(not(target_family = "wasm"))]
use assets::GalleryAssets;
use ely_gpui_component::theme::{Mode, Theme};
@@ -113,8 +114,8 @@ pub struct Start {
}
/// Sets Ely up and opens the gallery's window.
fn launch(start: Start, cx: &mut App) -> WindowHandle<shell::Gallery> {
ely_gpui_component::init(cx);
fn launch(start: Start, cx: &mut App) -> Result<WindowHandle<shell::Gallery>> {
ely_gpui_component::init(cx)?;
#[cfg(debug_assertions)]
ely_gpui_component::tooling::install_inspector(cx);
pages::bind_keys(cx);
@@ -142,10 +143,10 @@ fn launch(start: Start, cx: &mut App) -> WindowHandle<shell::Gallery> {
.open_window(options, |window, cx| {
cx.new(|cx| shell::Gallery::new(start, window, cx))
})
.expect("gallery window failed to open");
.context("gallery window failed to open")?;
menu_actions(window, cx);
cx.activate(true);
window
Ok(window)
}
#[cfg(not(target_family = "wasm"))]
@@ -204,7 +205,13 @@ fn main() -> Result<()> {
gpui_platform::application()
.with_assets(GalleryAssets)
.run(move |cx: &mut App| {
let window = launch(start, cx);
let window = match launch(start, cx) {
Ok(window) => window,
Err(error) => {
log::error!("gallery: {error:#}");
return cx.quit();
}
};
if let Some(dir) = capture {
capture::run(window, dir, page, scripted, cx);
}
+4 -1
View File
@@ -106,7 +106,10 @@ pub fn start() {
if let Some(choice) = EARLY.take() {
start.choice = choice;
}
let window = launch(start, cx);
let window = match launch(start, cx) {
Ok(window) => window,
Err(error) => return log::error!("gallery: {error:#}"),
};
HOST.with(|host| *host.borrow_mut() = Some((cx.to_async(), window)));
});
}
+2 -2
View File
@@ -6,8 +6,8 @@ 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` panics without them. |
| `ely_gpui_component::init(cx)` | Call once, before any window. 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. |
| `gpui_platform::application().with_assets(Assets)` | Ely embeds its icons and fonts. `init` returns an error without 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. |
+2 -2
View File
@@ -42,9 +42,9 @@ 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` panics unless `icons/check.svg` loads through it.
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.
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 panics on first draw. SVGs render as single-color masks; `IconName` and `IconPicker` continue to list the bundled icons.
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.
Compatible single-color SVGs can come from [Lucide](https://lucide.dev/), [Hugeicons](https://github.com/hugeicons/hugeicons), [Phosphor](https://github.com/phosphor-icons/core), [Remix Icon](https://github.com/Remix-Design/RemixIcon), or [Tabler](https://github.com/tabler/tabler-icons). The app supplies its chosen files and preserves their licenses; Ely does not download or register those catalogs.
+3 -3
View File
@@ -41,9 +41,9 @@ A window opens with a counter and a Press button. From a clone of the repository
| Symptom | Cause and fix |
| --- | --- |
| Panic: `` 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)`. |
| Panic: `ely: asset source failed` | The app's asset source returned an error. The panic carries it. |
| Panic: `ely: embedded fonts failed to register` | GPUI's text system refused Ely's fonts. File a bug with the log. |
| `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: 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` points at a revision other than Ely's. Use `1a28cff4b409169bac058bca40dfbfeb7621d19b`. |
| `metal shader compilation failed` | Default features are off and Xcode's Metal compiler is missing. Keep `runtime_shaders`. |
+1 -1
View File
@@ -8,7 +8,7 @@ Ely tests itself on GPUI's test platform: every chapter has tests, and `cargo te
## Testing an app that uses Ely
GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` panics there, and Ely's theme has no setup outside `init`. An app cannot draw Ely's components in a GPUI unit test today. Test the app's own state there, and see Ely's components in the gallery and in Ely's own tests.
GPUI's `TestAppContext` serves no assets, so `ely_gpui_component::init` returns an error there. Call `ely_gpui_component::init_for_tests(cx)` instead, under the `test-support` feature: it sets the theme and key bindings without assets or fonts.
## Writing a test inside Ely
+8 -4
View File
@@ -47,19 +47,23 @@ pub mod typography;
pub use assets::Assets;
use anyhow::Context as _;
use gpui::{App, KeyBinding};
use crate::primitives::{FocusNext, FocusPrev, IconName};
/// Loads fonts and the theme, binds Tab and text keys. Call once, first.
pub fn init(cx: &mut App) {
pub fn init(cx: &mut App) -> anyhow::Result<()> {
match cx.asset_source().load(IconName::Check.path()) {
Ok(Some(_)) => {}
Ok(None) => panic!("ely: pass `ely_gpui_component::Assets` to `Application::with_assets`"),
Err(error) => panic!("ely: asset source failed: {error:#}"),
Ok(None) => {
anyhow::bail!("ely: pass `ely_gpui_component::Assets` to `Application::with_assets`")
}
Err(error) => return Err(error.context("ely: asset source failed")),
}
assets::load_fonts(cx).expect("ely: embedded fonts failed to register");
assets::load_fonts(cx).context("ely: embedded fonts failed to register")?;
setup(cx);
Ok(())
}
/// `init` without assets or fonts, for app tests that render.
+42 -55
View File
@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::HashMap;
use gpui::{
App, AssetSource, Global, Hsla, IntoElement, Radians, RenderOnce, SharedString, Transformation,
@@ -362,7 +362,7 @@ impl Icon {
}
}
/// Draws an SVG supplied by the application's asset source; a missing one fails at render.
/// Draws an SVG supplied by the application's asset source; a missing one logs and shows `broken`.
pub fn from_path(path: impl Into<SharedString>) -> Self {
Self {
path: path.into(),
@@ -414,80 +414,67 @@ impl From<IconName> for Icon {
}
}
/// App paths already found in the asset source.
/// App paths already looked up, and whether the source held them.
#[derive(Default)]
struct FoundIcons(HashSet<SharedString>);
struct FoundIcons(HashMap<SharedString, bool>);
impl Global for FoundIcons {}
fn ensure_found(source: &dyn AssetSource, path: &str) {
fn found(source: &dyn AssetSource, path: &str) -> bool {
match source.load(path) {
Ok(Some(_)) => {}
Ok(None) => panic!("ely: icon {path} is not in the app's asset source"),
Err(error) => panic!("ely: icon {path} failed to load: {error:#}"),
Ok(Some(_)) => true,
Ok(None) => {
log::error!("ely: icon {path} is not in the app's asset source");
false
}
Err(error) => {
log::error!("ely: icon {path} failed to load: {error:#}");
false
}
}
}
impl Icon {
/// What a missing app icon shows: a broken picture in danger.
fn broken(self, cx: &App) -> Self {
Self {
path: IconName::ImageOff.path().into(),
bundled: true,
color: Some(cx.theme().colors.danger),
hover: None,
..self
}
}
}
impl RenderOnce for Icon {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
if !self.bundled && !cx.default_global::<FoundIcons>().0.contains(&self.path) {
ensure_found(cx.asset_source().as_ref(), &self.path);
cx.default_global::<FoundIcons>()
let source = cx.asset_source().clone();
let missing = !self.bundled
&& !*cx
.default_global::<FoundIcons>()
.0
.insert(self.path.clone());
}
.entry(self.path.clone())
.or_insert_with(|| found(source.as_ref(), &self.path));
let icon = if missing { self.broken(cx) } else { self };
let theme = cx.theme();
let color = self.color.unwrap_or(theme.colors.fg);
let color = icon.color.unwrap_or(theme.colors.fg);
svg()
.path(self.path)
.size(theme.icon_size(self.size))
.path(icon.path)
.size(theme.icon_size(icon.size))
.flex_none()
.text_color(color)
.when_some(self.hover, |svg, (group, hover)| {
.when_some(icon.hover, |svg, (group, hover)| {
svg.group_hover(group, |style| style.text_color(hover))
})
.when(self.rotation.is_some() || self.scale != 1.0, |svg| {
.when(icon.rotation.is_some() || icon.scale != 1.0, |svg| {
svg.with_transformation(
Transformation::rotate(self.rotation.unwrap_or(Radians(0.0)))
.with_scaling(size(self.scale, self.scale)),
Transformation::rotate(icon.rotation.unwrap_or(Radians(0.0)))
.with_scaling(size(icon.scale, icon.scale)),
)
})
}
}
#[cfg(test)]
mod tests {
use super::{Icon, IconName, ensure_found};
#[test]
fn every_icon_ships_in_the_bundle() {
for icon in IconName::ALL {
assert!(
crate::Assets::get(icon.path()).is_some(),
"{icon:?} missing at {}",
icon.path()
);
}
}
#[test]
fn an_app_path_draws_as_given_and_a_name_as_its_bundled_file() {
let custom = Icon::from_path("app-icons/mark.svg");
assert_eq!(custom.path.as_ref(), "app-icons/mark.svg");
assert!(!custom.bundled);
let named = Icon::new(IconName::Check);
assert_eq!(named.path.as_ref(), IconName::Check.path());
assert!(named.bundled);
}
#[test]
fn a_path_the_source_holds_is_found() {
ensure_found(&crate::Assets, IconName::Check.path());
}
#[test]
#[should_panic(expected = "ely: icon app-icons/missing.svg is not in the app's asset source")]
fn a_path_the_source_lacks_fails() {
ensure_found(&crate::Assets, "app-icons/missing.svg");
}
}
mod tests;
+70
View File
@@ -0,0 +1,70 @@
use gpui::{Context, IntoElement, Render, TestAppContext, Window, black};
use super::{FoundIcons, Icon, IconName, found};
use crate::theme::{ActiveTheme, Theme};
#[test]
fn every_icon_ships_in_the_bundle() {
for icon in IconName::ALL {
assert!(
crate::Assets::get(icon.path()).is_some(),
"{icon:?} missing at {}",
icon.path()
);
}
}
#[test]
fn an_app_path_draws_as_given_and_a_name_as_its_bundled_file() {
let custom = Icon::from_path("app-icons/mark.svg");
assert_eq!(custom.path.as_ref(), "app-icons/mark.svg");
assert!(!custom.bundled);
let named = Icon::new(IconName::Check);
assert_eq!(named.path.as_ref(), IconName::Check.path());
assert!(named.bundled);
}
#[test]
fn the_source_answers_whether_it_holds_a_path() {
assert!(found(&crate::Assets, IconName::Check.path()));
assert!(!found(&crate::Assets, "app-icons/missing.svg"));
}
struct Missing;
impl Render for Missing {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Icon::from_path("app-icons/missing.svg")
}
}
#[gpui::test]
fn a_missing_app_icon_draws_its_mark(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Missing);
cx.run_until_parked();
let seen = cx.update(|_, cx| cx.global::<FoundIcons>().0.clone());
assert_eq!(seen.get("app-icons/missing.svg"), Some(&false));
}
#[gpui::test]
fn a_broken_icon_shows_a_broken_picture_in_danger(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
let icon = Icon::from_path("app-icons/missing.svg")
.color(black())
.group_hover_color("row", black())
.broken(cx);
assert_eq!(icon.path.as_ref(), IconName::ImageOff.path());
assert_eq!(icon.color, Some(cx.theme().colors.danger));
assert!(icon.hover.is_none(), "hover keeps the danger color");
});
}
#[gpui::test]
fn init_without_ely_assets_returns_an_error(cx: &mut TestAppContext) {
let error = cx
.update(crate::init)
.expect_err("the test source holds no Ely assets");
assert!(format!("{error:#}").contains("pass `ely_gpui_component::Assets`"));
}
+3 -2
View File
@@ -6,14 +6,14 @@ Rules, at the owner's word:
- A handler a component needs is an argument of `new`. Leaving it out fails to compile, not at render.
- A state live data or the outside world can reach (a key gone from a list that changed, a failed load) logs an error naming the owner and draws a visible state: nothing chosen, or an error line in the component's box. The app keeps running.
- An assert on a caller's argument, or on a fact true by construction, stays, as std's do. One that runtime data or input can trip is fixed.
- An assert on a caller's argument, or on a fact true by construction, stays, as std's do. One that runtime data or input can trip is fixed. Reachable means what a component meets on its own: input inside it, its own later state, a call to the outside, or two arguments that agree at one time and not the next (a selected key and its list). One value checked against itself (a ratio above zero) is the caller's contract.
Each item reads every site in its folders, fixes what the rules name, updates callers in the gallery, docs examples and tests, passes `scripts/check.sh`, and passes a codex review (`gpt-6.1-sol`, effort `high`) in a herdr split, five rounds at most. Then it commits and pushes.
## Items
- [x] P00 Policy in AGENTS.md
- [ ] P01 Primitives — `src/primitives`, `src/lib.rs`, `src/assets.rs`
- [x] P01 Primitives — `src/primitives`, `src/lib.rs`, `src/assets.rs`
- [ ] P02 Typography — `src/typography`
- [ ] P03 Layout — `src/layout`
- [ ] P04 Window & Shell — `src/shell`
@@ -63,3 +63,4 @@ Each item reads every site in its folders, fixes what the rules name, updates ca
One line per item: what changed, what stayed and why, review rounds.
- P00: AGENTS.md holds the three rules; this list follows the PRD's chapters. Review: 2 rounds (round 1 found the runtime clause missing).
- P01: `init` returns `anyhow::Result`; a missing app icon logs once and shows `Icon::broken`. Kept: `layer.rs` expects (gpui's element order), `checked_ratio` (one value, the caller's). Review: 2 rounds.