Never crash: ui-egui is panic-free (Rule 9) (#57)

- canvas: partial redraws read last_damage with unwrap_or(EMPTY), and the
  GPU cache is looked up with let-else after the upload.
- icons: a missing icon (and missing fallback) yields empty bytes, which
  egui shows as a broken image, instead of expect.
- type tool: the caret update is an if-let instead of expect("editing").
- Variables dialog: pages return early if their JSON state is malformed
  instead of unwrap.
- parity: the same hunk as #40.
- ui-egui denies unwrap/expect/panic/unimplemented/todo/unreachable
  (a one-line attribute in lib.rs).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-04 13:40:17 -04:00 committed by GitHub
parent 6dffa0894c
commit 86cc989e72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 14 additions and 12 deletions

View File

@ -218,7 +218,7 @@ pub fn ensure_texture(app: &mut PhotocraftApp, ctx: &egui::Context, idx: usize)
&& last_damage.is_some(); && last_damage.is_some();
let t0 = crate::gpu_canvas::now_ms(); let t0 = crate::gpu_canvas::now_ms();
if partial { if partial {
let r = last_damage.unwrap().intersect(&doc.bounds()); let r = last_damage.unwrap_or(DRect::EMPTY).intersect(&doc.bounds());
if !r.is_empty() { if !r.is_empty() {
let buf = photocraft_compose::render(&doc, r); let buf = photocraft_compose::render(&doc, r);
let t1 = crate::gpu_canvas::now_ms(); let t1 = crate::gpu_canvas::now_ms();
@ -313,7 +313,7 @@ fn ensure_gpu(app: &mut PhotocraftApp, idx: usize) -> bool {
} }
} }
if partial && !done { if partial && !done {
let r = last_damage.unwrap().intersect(&doc.bounds()); let r = last_damage.unwrap_or(DRect::EMPTY).intersect(&doc.bounds());
if r.is_empty() { if r.is_empty() {
done = true; done = true;
} else { } else {
@ -331,7 +331,7 @@ fn ensure_gpu(app: &mut PhotocraftApp, idx: usize) -> bool {
gpu.upload_buffer_full(id.0, &full); gpu.upload_buffer_full(id.0, &full);
app.perf.record("full", size[0] as u64 * size[1] as u64, t1 - t0, crate::gpu_canvas::now_ms() - t1); app.perf.record("full", size[0] as u64 * size[1] as u64, t1 - t0, crate::gpu_canvas::now_ms() - t1);
} }
let cache = app.canvases.get_mut(&id).expect("inserted above"); let Some(cache) = app.canvases.get_mut(&id) else { return true };
cache.revision = revision; cache.revision = revision;
cache.preview_key = preview_key; cache.preview_key = preview_key;
cache.on_gpu = true; cache.on_gpu = true;

View File

@ -32,7 +32,7 @@ pub fn exists(name: &str) -> bool {
/// An egui image for an icon, tinted. /// An egui image for an icon, tinted.
pub fn image(name: &str, size: f32, tint: Color32) -> egui::Image<'static> { pub fn image(name: &str, size: f32, tint: Color32) -> egui::Image<'static> {
let bytes = white_icons().get(name).or_else(|| white_icons().get("square")).cloned().expect("square icon present"); let bytes = white_icons().get(name).or_else(|| white_icons().get("square")).cloned().unwrap_or_default();
egui::Image::from_bytes(format!("bytes://icons/{name}.svg"), egui::load::Bytes::Shared(bytes)).fit_to_exact_size(Vec2::splat(size)).tint(tint) egui::Image::from_bytes(format!("bytes://icons/{name}.svg"), egui::load::Bytes::Shared(bytes)).fit_to_exact_size(Vec2::splat(size)).tint(tint)
} }

View File

@ -5,6 +5,7 @@
//! data). The [`control`] module exposes both to automation, so agents can drive and inspect every //! data). The [`control`] module exposes both to automation, so agents can drive and inspect every
//! part of the interface. //! part of the interface.
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod actions; pub mod actions;
pub mod adjust_ui; pub mod adjust_ui;

View File

@ -85,12 +85,13 @@ pub fn pointer_down(app: &mut PhotocraftApp, x: f64, y: f64, shift: bool) -> boo
let id = LayerId(ed.layer); let id = LayerId(ed.layer);
if hit_layer(app, x, y) == Some(id) { if hit_layer(app, x, y) == Some(id) {
let off = hit_offset(app, id, x, y); let off = hit_offset(app, id, x, y);
let e = app.ui.text_edit.as_mut().expect("editing"); if let Some(e) = app.ui.text_edit.as_mut() {
e.caret = off; e.caret = off;
if !shift { if !shift {
e.anchor = off; e.anchor = off;
}
e.dragging = true;
} }
e.dragging = true;
return true; return true;
} }
commit(app); commit(app);

View File

@ -138,7 +138,7 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, fields: &mut Map<String,
} }
fn define_page(ui: &mut egui::Ui, _t: &Tokens, layers: &[(u64, String, bool)], state: &mut Value) { fn define_page(ui: &mut egui::Ui, _t: &Tokens, layers: &[(u64, String, bool)], state: &mut Value) {
let defs = state.get_mut("defs").and_then(Value::as_array_mut).unwrap(); let Some(defs) = state.get_mut("defs").and_then(Value::as_array_mut) else { return };
ui.label("Variables bind a layer's visibility, text or pixels to a named data slot."); ui.label("Variables bind a layer's visibility, text or pixels to a named data slot.");
ui.add_space(4.0); ui.add_space(4.0);
let mut remove = None; let mut remove = None;
@ -199,7 +199,7 @@ fn data_sets_page(ui: &mut egui::Ui, _t: &Tokens, state: &mut Value, fields: &mu
ui.weak("Define at least one variable first."); ui.weak("Define at least one variable first.");
return; return;
} }
let sets = state.get_mut("dataSets").and_then(Value::as_array_mut).unwrap(); let Some(sets) = state.get_mut("dataSets").and_then(Value::as_array_mut) else { return };
let mut cur = fields.get("__cur").and_then(Value::as_u64).unwrap_or(0) as usize; let mut cur = fields.get("__cur").and_then(Value::as_u64).unwrap_or(0) as usize;
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -233,7 +233,7 @@ fn data_sets_page(ui: &mut egui::Ui, _t: &Tokens, state: &mut Value, fields: &mu
} }
}); });
ui.add_space(4.0); ui.add_space(4.0);
let values = set.get_mut("values").and_then(Value::as_array_mut).unwrap(); let Some(values) = set.get_mut("values").and_then(Value::as_array_mut) else { return };
for (vname, vty) in &def_meta { for (vname, vty) in &def_meta {
// find or create a value entry for this variable // find or create a value entry for this variable
let idx = values.iter().position(|v| v.get("variable").and_then(Value::as_str) == Some(vname)); let idx = values.iter().position(|v| v.get("variable").and_then(Value::as_str) == Some(vname));