From 2c46e97a48996ec4dc551b2a99fc18718bab45f9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 11:39:35 -0400 Subject: [PATCH] Never crash: UI The canvas, Properties panel and QR dialog skip a frame instead of expect()ing a document; the control channel caps ui.drag steps. ui-egui denies the panicking lints. Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 4 ++++ crates/ui-egui/src/canvas.rs | 10 +++++----- crates/ui-egui/src/control.rs | 3 ++- crates/ui-egui/src/dialogs.rs | 3 +-- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/panels/properties.rs | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..4b6c79f --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index db4d139..3d16058 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -245,7 +245,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { } handle_input(app, ui, &resp, rect); let Some(st) = app.session.active() else { return }; - let v = *app.view().expect("view"); + let Some(&v) = app.view() else { return }; let xf = Xf::new(rect, &v); let doc = st.doc.clone(); let layout = CanvasLayout::new(&doc, st.editing_parents); @@ -400,7 +400,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf, let doc_changed = app.canvas.shown.is_none_or(|s| s.doc != doc_key); let zoom_same = app.canvas.shown.is_some_and(|s| (s.zoom - xf.zoom).abs() < 1e-9); let job = |app: &DesignApp| { - let st = app.session.active().expect("doc"); + let st = app.session.active()?; let placed: Vec = layout.slots.iter().map(|s| designcraft_render::Placed { spread: s.spread, xf: s.xf }).collect(); let w = (target.size.0 as f64 * ppp).round().max(1.0) as u32; @@ -424,7 +424,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf, blend_space_view: !app.ui.proof_colors, ..Default::default() }; - (st.doc.clone(), placed, w, h, view, opts) + Some((st.doc.clone(), placed, w, h, view, opts)) }; // Synchronous path: document edits (keep editing crisp), first frame, or no worker. #[cfg(not(target_arch = "wasm32"))] @@ -442,7 +442,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf, } if !has_worker || app.canvas.shown.is_none() || (doc_changed && zoom_same) { let t0 = crate::now_ms(); - let (doc, placed, w, h, view, opts) = job(app); + let Some((doc, placed, w, h, view, opts)) = job(app) else { return }; let img = app.canvas.renderer.render(&doc, &app.session.cache, &placed, w, h, view, &opts); upload(app, ctx, img); app.canvas.shown = Some(target); @@ -463,7 +463,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf, }) { return; } - let (doc, placed, w, h, view, opts) = job(app); + let Some((doc, placed, w, h, view, opts)) = job(app) else { return }; app.canvas.pending_doc = Some(doc.clone()); app.canvas.token += 1; let token = app.canvas.token; diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 6c3bf74..7ff3d79 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -198,7 +198,8 @@ pub fn handle(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest) -> app.synthetic.push(egui::Event::PointerMoved(a)); app.synthetic.push(egui::Event::PointerButton { pos: a, button, pressed: true, modifiers }); if req.method == "ui.drag" { - let steps = p.get("steps").and_then(Value::as_u64).unwrap_or(8).max(1); + // From the caller: a huge count would queue events until memory runs out. + let steps = p.get("steps").and_then(Value::as_u64).unwrap_or(8).clamp(1, 10_000); for i in 1..=steps { let t = i as f32 / steps as f32; app.synthetic.push(egui::Event::PointerMoved(a + (end - a) * t)); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 95b3ec5..1c95dc7 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1484,8 +1484,7 @@ pub fn confirm(app: &mut DesignApp) -> Result { "qrCode" => { let mut p = Value::Object(d.fields.clone()); // Nothing selected: a 2-inch code at the top left of the page in view. - if app.session.active().is_some_and(|s| s.selection.items.is_empty()) { - let st = app.session.active().expect("doc"); + if let Some(st) = app.session.active().filter(|s| s.selection.items.is_empty()) { let abs = crate::canvas::current_page(app).unwrap_or(0); let (si, pi) = st.doc.page_loc(abs).unwrap_or((0, 0)); let x = st.doc.spreads.get(si).and_then(|sp| sp.pages.get(pi)).map_or(0.0, |pg| pg.x); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 8dc8171..13d37c0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -3,6 +3,7 @@ //! The UI is thin: every action goes through [`DesignApp::run`], which dispatches UI commands //! (view/window) here and everything else to the engine. Menus, shortcuts, the ⌘K palette and //! the control channel ([`control`]) share that entry point. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] #![forbid(unsafe_code)] pub mod about; diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index bb29f41..06deaf1 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -830,7 +830,7 @@ fn text_style_section(app: &mut DesignApp, ui: &mut Ui) { } ui.add_space(5.0); let (names, cur): (Vec, String) = { - let st = app.session.active().expect("doc"); + let Some(st) = app.session.active() else { return }; if tab == 0 { ( st.doc.styles.paragraph.iter().map(|p| p.name.clone()).filter(|n| n != designcraft_doc::NO_PARA_STYLE).collect(),