From f66588709483f09a1629e596bd70119b792640ad Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sat, 24 Oct 2026 02:18:38 +0000 Subject: [PATCH] chore: resolve SonarCloud maintainability issues --- apps/deckcraft-web/index.html | 2 +- crates/anim/src/path.rs | 2 +- crates/av1/src/tile.rs | 6 +- crates/engine/src/cmd/insert.rs | 60 ++++++----- crates/engine/src/cmd/slide.rs | 4 +- crates/engine/src/cmd/smartart.rs | 2 +- crates/engine/src/guard.rs | 2 +- crates/engine/src/icons.rs | 2 +- crates/engine/src/recovery.rs | 4 +- crates/fonts/src/fontdb.rs | 54 +++++----- crates/format/src/lib.rs | 52 +++++----- crates/h264/src/cabac_mb.rs | 2 +- crates/media/src/player.rs | 2 +- crates/metafile/src/emfplus.rs | 2 +- crates/metafile/src/tests.rs | 2 +- crates/model/src/chart.rs | 147 ++++++++++++++++----------- crates/model/src/lib.rs | 6 +- crates/model/src/style.rs | 2 +- crates/model/src/text.rs | 4 +- crates/model/src/trendline.rs | 161 ++++++++++++++++-------------- crates/opus/src/celt/bands.rs | 2 +- crates/opus/src/celt/mod.rs | 2 +- crates/opus/src/decoder.rs | 2 +- crates/pdf/src/lib.rs | 50 ++++++---- crates/render/src/images.rs | 2 +- crates/text/src/lib.rs | 4 +- crates/ui-egui/src/dialogs.rs | 7 +- crates/ui-egui/src/ribbon.rs | 60 ++++++----- packaging/linux/package.sh | 9 +- xtask/src/assets.rs | 2 +- xtask/src/layers.rs | 2 +- 31 files changed, 364 insertions(+), 296 deletions(-) diff --git a/apps/deckcraft-web/index.html b/apps/deckcraft-web/index.html index b1834c33..852d0202 100644 --- a/apps/deckcraft-web/index.html +++ b/apps/deckcraft-web/index.html @@ -2,7 +2,7 @@ - + DeckCraft diff --git a/crates/anim/src/path.rs b/crates/anim/src/path.rs index af61bd58..ec31628c 100644 --- a/crates/anim/src/path.rs +++ b/crates/anim/src/path.rs @@ -49,7 +49,7 @@ fn tokenize(s: &str) -> Vec { n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit()) } { i += 2; - while cs.get(i).is_some_and(|d| d.is_ascii_digit()) { + while cs.get(i).is_some_and(char::is_ascii_digit) { i += 1; } break; diff --git a/crates/av1/src/tile.rs b/crates/av1/src/tile.rs index 5d73688b..c8446795 100644 --- a/crates/av1/src/tile.rs +++ b/crates/av1/src/tile.rs @@ -2222,18 +2222,16 @@ fn neg_deinterleave(diff: i32, r: i32, max: i32) -> i32 { if diff <= 2 * r { if diff & 1 == 1 { return r + ((diff + 1) >> 1); - } else { - return r - (diff >> 1); } + return r - (diff >> 1); } diff } else { if diff <= 2 * (max - r - 1) { if diff & 1 == 1 { return r + ((diff + 1) >> 1); - } else { - return r - (diff >> 1); } + return r - (diff >> 1); } max - (diff + 1) } diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 149957df..5bbe995c 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -1761,6 +1761,37 @@ fn chart_data(s: &mut Session, p: &Value) -> Result { }) } +fn apply_bin_options(c: &mut deckcraft_model::chart::Chart, bin_count: Option>, bin_size: Option>) { + if let Some(n) = bin_count { + c.bin_count = n; + if n.is_some() { + c.bin_size = None; + } + } + if let Some(w) = bin_size { + c.bin_size = w; + if w.is_some() { + c.bin_count = None; + } + } +} + +fn apply_subtotals_and_bars(c: &mut deckcraft_model::chart::Chart, p: &Value, subtotals: Option<&[usize]>) { + if let Some(t) = subtotals { + let n = c.series.first().map_or(0, |s| s.values.len()); + let mut t: Vec = t.iter().copied().filter(|i| *i < n).collect(); + t.sort_unstable(); + t.dedup(); + c.subtotals = t; + } + if let Some(b) = p.get("upDownBars").and_then(Value::as_bool) { + c.up_down_bars = b; + } + if let Some(q) = p.get("quartiles").and_then(Value::as_str) { + c.inclusive_quartiles = q.eq_ignore_ascii_case("inclusive"); + } +} + fn chart_options(s: &mut Session, p: &Value) -> Result { // Histogram bins: a count or a width (setting one clears the other; null for automatic). let bin_count = p.get("binCount").map(|v| v.as_u64().map(|n| n.clamp(1, deckcraft_model::chart::MAX_BINS as u64) as u32)); @@ -1772,33 +1803,8 @@ fn chart_options(s: &mut Session, p: &Value) -> Result { let subtotals: Option> = p.get("subtotals").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect()); with_chart(s, p, "chart.options", |c| { - if let Some(n) = bin_count { - c.bin_count = n; - if n.is_some() { - c.bin_size = None; - } - } - if let Some(w) = bin_size { - c.bin_size = w; - if w.is_some() { - c.bin_count = None; - } - } - if let Some(t) = &subtotals { - let n = c.series.first().map_or(0, |s| s.values.len()); - let mut t: Vec = t.iter().copied().filter(|i| *i < n).collect(); - t.sort_unstable(); - t.dedup(); - c.subtotals = t; - } - if let Some(b) = p.get("upDownBars").and_then(Value::as_bool) { - c.up_down_bars = b; - c.raw = None; - } - if let Some(q) = p.get("quartiles").and_then(Value::as_str) { - c.inclusive_quartiles = q.eq_ignore_ascii_case("inclusive"); - c.raw = None; - } + apply_bin_options(c, bin_count, bin_size); + apply_subtotals_and_bars(c, p, subtotals.as_deref()); if bin_count.is_some() || bin_size.is_some() || subtotals.is_some() { c.raw = None; } diff --git a/crates/engine/src/cmd/slide.rs b/crates/engine/src/cmd/slide.rs index 777c479c..8ab3b3c3 100644 --- a/crates/engine/src/cmd/slide.rs +++ b/crates/engine/src/cmd/slide.rs @@ -539,9 +539,9 @@ fn section_add(s: &mut Session, p: &Value) -> Result { if doc.sections.is_empty() { // The slides before `at` go into a "Default Section". if at > 0 { - doc.sections.push(Section { name: "Default Section".into(), slides: ids.get(..at).map(|x| x.to_vec()).unwrap_or_default() }); + doc.sections.push(Section { name: "Default Section".into(), slides: ids.get(..at).map(<[_]>::to_vec).unwrap_or_default() }); } - doc.sections.push(Section { name, slides: ids.get(at..).map(|x| x.to_vec()).unwrap_or_default() }); + doc.sections.push(Section { name, slides: ids.get(at..).map(<[_]>::to_vec).unwrap_or_default() }); } else { let Some(start) = ids.get(at).copied() else { return Err(bad("section.add", "no slide")) }; let si = doc.section_of(start).unwrap_or(0); diff --git a/crates/engine/src/cmd/smartart.rs b/crates/engine/src/cmd/smartart.rs index c58d1830..c6e69a85 100644 --- a/crates/engine/src/cmd/smartart.rs +++ b/crates/engine/src/cmd/smartart.rs @@ -14,7 +14,7 @@ use deckcraft_model::{ }; use serde_json::{Value, json}; -use super::*; +use super::{bad, has_selection, str_param, xfrm_of, CommandSpec}; use crate::{EngineError, Result, Session}; /// Deepest outline level the Text Pane allows. diff --git a/crates/engine/src/guard.rs b/crates/engine/src/guard.rs index 87d1fa7c..1c2ceac9 100644 --- a/crates/engine/src/guard.rs +++ b/crates/engine/src/guard.rs @@ -13,7 +13,7 @@ use crate::{EngineError, Result, Session}; pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { payload .downcast_ref::<&str>() - .map(|s| s.to_string()) + .map(ToString::to_string) .or_else(|| payload.downcast_ref::().cloned()) .unwrap_or_else(|| "unknown panic".into()) } diff --git a/crates/engine/src/icons.rs b/crates/engine/src/icons.rs index 89c72180..bdd46289 100644 --- a/crates/engine/src/icons.rs +++ b/crates/engine/src/icons.rs @@ -60,7 +60,7 @@ pub fn stroke_width(icon: &Icon, w: f64, h: f64) -> f64 { } fn parsed() -> &'static HashMap<&'static str, BezPath> { - static CACHE: OnceLock> = OnceLock::new(); + static CACHE: OnceLock> = OnceLock::new(); CACHE.get_or_init(|| { let mut m = HashMap::new(); for icon in ICONS { diff --git a/crates/engine/src/recovery.rs b/crates/engine/src/recovery.rs index 646a1434..ac35cb13 100644 --- a/crates/engine/src/recovery.rs +++ b/crates/engine/src/recovery.rs @@ -152,13 +152,13 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> { let Ok(rd) = std::fs::read_dir(dir) else { return out }; for e in rd.flatten() { let p = e.path(); - if p.extension().and_then(|x| x.to_str()) != Some("deckcraft") { + if p.extension().and_then(std::ffi::OsStr::to_str) != Some("deckcraft") { continue; } if !std::fs::metadata(&p).is_ok_and(|m| m.is_file()) { continue; } - let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::().ok()) else { continue }; + let Some(uid) = p.file_stem().and_then(std::ffi::OsStr::to_str).and_then(|s| s.parse::().ok()) else { continue }; // Metadata is a file on disk: anything but an object (corrupt, hand-edited) counts as none. let meta = read_regular(&p.with_extension("json"), MAX_META_BYTES) .and_then(|t| serde_json::from_slice::(&t).ok()) diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 03267384..4844bd30 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -161,12 +161,12 @@ pub struct FaceRef(&'static FontFace); impl FaceRef { /// The handle for a loaded face (memoised per face). pub fn of(face: &Arc) -> FaceRef { - static LEAKED: RwLock>> = RwLock::new(Vec::new()); + static LEAKED: RwLock>> = RwLock::new(Vec::new()); let id = face.id as usize; - if let Some(Some(f)) = LEAKED.read().unwrap_or_else(|e| e.into_inner()).get(id) { + if let Some(Some(f)) = LEAKED.read().unwrap_or_else(std::sync::PoisonError::into_inner).get(id) { return FaceRef(f); } - let mut w = LEAKED.write().unwrap_or_else(|e| e.into_inner()); + let mut w = LEAKED.write().unwrap_or_else(std::sync::PoisonError::into_inner); if w.len() <= id { w.resize(id + 1, None); } @@ -743,7 +743,7 @@ pub fn base_style(style: &str) -> &str { } fn norm(s: &str) -> String { - s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect() + s.chars().filter(char::is_alphanumeric).flat_map(char::to_lowercase).collect() } /// Weight implied by a style name. @@ -807,7 +807,7 @@ impl FontDb { pub fn set_system_fallback(&self, on: bool) { #[cfg(not(target_arch = "wasm32"))] { - self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled = on; + self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled = on; } #[cfg(target_arch = "wasm32")] let _ = on; @@ -822,14 +822,14 @@ impl FontDb { } pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec>> { - self.faces.read().unwrap_or_else(|e| e.into_inner()) + self.faces.read().unwrap_or_else(std::sync::PoisonError::into_inner) } /// The system font catalog, scanned first if it hasn't been yet. #[cfg(not(target_arch = "wasm32"))] fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec> { self.ensure_catalog(); - self.catalog.read().unwrap_or_else(|e| e.into_inner()) + self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner) } /// Family names available (loaded plus cataloged system fonts), sorted and deduplicated. @@ -885,7 +885,7 @@ impl FontDb { } } } - let mut faces = self.faces.write().unwrap_or_else(|e| e.into_inner()); + let mut faces = self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner); let mut added = 0; for f in new { // Checked again under the lock: another thread may have added the same face. @@ -923,7 +923,7 @@ impl FontDb { #[cfg(not(target_arch = "wasm32"))] pub fn load_system_fonts(&self) -> usize { // Families load again from the new catalog when next looked up. - self.loaded_families.lock().unwrap_or_else(|e| e.into_inner()).clear(); + self.loaded_families.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clear(); // The first scan, or a rescan once it is done (never both at once). let mut first = None; self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs())); @@ -959,7 +959,7 @@ impl FontDb { } continue; } - let ext = p.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase()); + let ext = p.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase); if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) { continue; } @@ -975,7 +975,7 @@ impl FontDb { } let n = found.len(); log::debug!("cataloged {n} system font faces"); - *self.catalog.write().unwrap_or_else(|e| e.into_inner()) = found; + *self.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner) = found; n } @@ -983,7 +983,7 @@ impl FontDb { /// it is loaded. #[cfg(not(target_arch = "wasm32"))] fn load_cataloged(&self, family: &str) -> bool { - let loading = self.loading.lock().unwrap_or_else(|e| e.into_inner()); + let loading = self.loading.lock().unwrap_or_else(std::sync::PoisonError::into_inner); self.load_family(family, &loading) } @@ -1047,7 +1047,7 @@ impl FontDb { // them rather than finding none and falling back to another family. #[cfg(not(target_arch = "wasm32"))] { - let mut loaded = self.loaded_families.lock().unwrap_or_else(|e| e.into_inner()); + let mut loaded = self.loaded_families.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let key = family.to_lowercase(); if !loaded.contains(&key) { self.load_cataloged(family); @@ -1112,7 +1112,7 @@ impl FontDb { } let f = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?; let f = Arc::new(f); - self.faces.write().unwrap_or_else(|e| e.into_inner()).push(f.clone()); + self.faces.write().unwrap_or_else(std::sync::PoisonError::into_inner).push(f.clone()); Some(f) } @@ -1214,20 +1214,20 @@ impl FontDb { } let seen = self.read_faces().len(); { - let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner()); + let sys = self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner); if !sys.enabled || sys.misses.contains(&c) { return false; } } let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c) && accept(f)); let cataloged: Vec = self.read_catalog().iter().map(|e| e.family.clone()).collect(); - let loading = self.loading.lock().unwrap_or_else(|e| e.into_inner()); + let loading = self.loading.lock().unwrap_or_else(std::sync::PoisonError::into_inner); // Another thread may have loaded a face for `c` (faces are only ever added), or found // there is none, while this one waited. if self.read_faces().len() != seen && covered(self) { return true; } - if self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.contains(&c) { + if self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).misses.contains(&c) { return false; } for fam in SYSTEM_FALLBACKS { @@ -1238,7 +1238,7 @@ impl FontDb { } } } - let mut paths: Vec = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.path.clone()).collect(); + let mut paths: Vec = self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner).iter().map(|e| e.path.clone()).collect(); paths.sort(); paths.dedup(); for p in paths { @@ -1253,7 +1253,7 @@ impl FontDb { } } if remember_miss { - self.sys.lock().unwrap_or_else(|e| e.into_inner()).misses.insert(c); + self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).misses.insert(c); } false } @@ -1326,10 +1326,10 @@ impl FontDb { if c.is_control() || c.is_whitespace() { return false; } - if !self.sys.lock().unwrap_or_else(|e| e.into_inner()).enabled { + if !self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner).enabled { return false; } - if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) { + if MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).contains(&c) { return false; } let mut paths: Vec = self.read_catalog().iter().map(|e| e.path.clone()).collect(); @@ -1347,7 +1347,7 @@ impl FontDb { return true; } } - MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c); + MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).insert(c); false } @@ -1359,12 +1359,12 @@ impl FontDb { return false; } { - let sys = self.sys.lock().unwrap_or_else(|e| e.into_inner()); + let sys = self.sys.lock().unwrap_or_else(std::sync::PoisonError::into_inner); if !sys.enabled { return false; } } - if MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).contains(&c) { + if MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).contains(&c) { return false; } let mut paths: Vec = self.read_catalog().iter().map(|e| e.path.clone()).collect(); @@ -1382,14 +1382,14 @@ impl FontDb { return true; } } - MISSES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).insert(c); + MISSES.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get_or_insert_with(Default::default).insert(c); false } /// Glyph outline in font units, y-down (flipped), cached per (face, glyph). pub fn outline(&self, face: &FontFace, gid: u32) -> Arc { let key = (face.id, gid); - if let Some(p) = self.outlines.lock().unwrap_or_else(|e| e.into_inner()).get(&key) { + if let Some(p) = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&key) { return p.clone(); } let mut pen = FlipPen(BezPath::new()); @@ -1399,7 +1399,7 @@ impl FontDb { let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen); } let p = Arc::new(pen.0); - let mut cache = self.outlines.lock().unwrap_or_else(|e| e.into_inner()); + let mut cache = self.outlines.lock().unwrap_or_else(std::sync::PoisonError::into_inner); if cache.len() >= OUTLINE_CACHE_MAX { cache.clear(); } diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 4512b445..73712d28 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -1070,33 +1070,33 @@ mod tests { assert_eq!(md_list_item(not), None, "{not:?}"); } } -} -/// #814: slides_to_outline includes table cell text, which it used to skip. -#[test] -fn outline_includes_table_text() { - use deckcraft_model::table::{Cell, Row, Table}; - use deckcraft_model::{Shape, ShapeKind, Xfrm}; - let mut p = defaults::new_presentation(None); - let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap(); - let mut s = defaults::new_slide(&mut p, layout).expect("slide"); - s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title())); - let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() }; - body.text = Some(TextBody::from_text("Text box body")); - s.shapes.push(body); + /// #814: slides_to_outline includes table cell text, which it used to skip. + #[test] + fn outline_includes_table_text() { + use deckcraft_model::table::{Cell, Row, Table}; + use deckcraft_model::{Shape, ShapeKind, Xfrm}; + let mut p = defaults::new_presentation(None); + let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap(); + let mut s = defaults::new_slide(&mut p, layout).expect("slide"); + s.shapes.retain(|sh| !sh.ph_type().is_some_and(|k| k.is_title())); + let mut body = Shape { xfrm: Some(Xfrm::new(0.0, 0.0, 600.0, 100.0)), ..Default::default() }; + body.text = Some(TextBody::from_text("Text box body")); + s.shapes.push(body); - let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() }; - for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] { - let cells: Vec = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect(); - table.rows.push(Row { height: 30.0, cells }); + let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() }; + for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] { + let cells: Vec = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect(); + table.rows.push(Row { height: 30.0, cells }); + } + let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() }; + tbl.kind = ShapeKind::Table(table); + s.shapes.push(tbl); + p.slides = vec![std::sync::Arc::new(s)]; + + let out = slides_to_outline(&p); + assert!(out.contains("Text box body"), "{out}"); + assert!(out.contains("Cell A1\tCell B1"), "{out}"); + assert!(out.contains("Cell A2\tCell B2"), "{out}"); } - let mut tbl = Shape { xfrm: Some(Xfrm::new(0.0, 100.0, 400.0, 60.0)), ..Default::default() }; - tbl.kind = ShapeKind::Table(table); - s.shapes.push(tbl); - p.slides = vec![std::sync::Arc::new(s)]; - - let out = slides_to_outline(&p); - assert!(out.contains("Text box body"), "{out}"); - assert!(out.contains("Cell A1\tCell B1"), "{out}"); - assert!(out.contains("Cell A2\tCell B2"), "{out}"); } diff --git a/crates/h264/src/cabac_mb.rs b/crates/h264/src/cabac_mb.rs index f1bf6f0d..2873c4a7 100644 --- a/crates/h264/src/cabac_mb.rs +++ b/crates/h264/src/cabac_mb.rs @@ -244,7 +244,7 @@ impl SliceDecoder<'_> { while prefix < 9 && c.decode_decision(base + (prefix + 2).min(6)) == 1 { prefix += 1; } - let mut val = prefix as i32; + let mut val = prefix; if prefix >= 9 { // EG3 suffix let mut k = 3; diff --git a/crates/media/src/player.rs b/crates/media/src/player.rs index dbb9d2c0..3c972f2c 100644 --- a/crates/media/src/player.rs +++ b/crates/media/src/player.rs @@ -254,7 +254,7 @@ impl Player { } fn with(&self, f: impl FnOnce(&mut Mixer) -> R) -> R { - let mut g = self.mixer.lock().unwrap_or_else(|e| e.into_inner()); + let mut g = self.mixer.lock().unwrap_or_else(std::sync::PoisonError::into_inner); f(&mut g) } diff --git a/crates/metafile/src/emfplus.rs b/crates/metafile/src/emfplus.rs index 550c2d18..3324f5f9 100644 --- a/crates/metafile/src/emfplus.rs +++ b/crates/metafile/src/emfplus.rs @@ -1187,7 +1187,7 @@ fn image(r: Bytes) -> Option { fn metafile(r: Bytes) -> Option { let size = r.u32(12)? as usize; let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?; - let depth = NESTING.with(|n| n.get()); + let depth = NESTING.with(std::cell::Cell::get); if depth >= MAX_NESTING { return None; } diff --git a/crates/metafile/src/tests.rs b/crates/metafile/src/tests.rs index fb7dfe36..09e76413 100644 --- a/crates/metafile/src/tests.rs +++ b/crates/metafile/src/tests.rs @@ -1716,7 +1716,7 @@ fn plus_pic(recs: Vec>) -> Picture { } fn u16s(s: &str) -> Vec { - s.encode_utf16().flat_map(|u| u.to_le_bytes()).collect() + s.encode_utf16().flat_map(u16::to_le_bytes).collect() } /// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white. diff --git a/crates/model/src/chart.rs b/crates/model/src/chart.rs index 5ff72cf4..8f3c23fa 100644 --- a/crates/model/src/chart.rs +++ b/crates/model/src/chart.rs @@ -491,50 +491,71 @@ impl Default for Chart { } impl Chart { + fn sample_treemap_sunburst(kind: ChartType) -> Self { + let mut c = Chart { + kind, + categories: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"].iter().map(ToString::to_string).collect(), + category_levels: vec![["Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q3", "Q3"].iter().map(ToString::to_string).collect()], + vary_colors: true, + ..Default::default() + }; + let v = [4.2, 2.8, 3.6, 4.7, 2.6, 4.1, 1.9, 3.0]; + c.series.push(Series { name: "Series 1".into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() }); + c + } + + fn sample_observation_chart(kind: ChartType) -> Self { + let obs = [12.0, 15.0, 17.0, 18.0, 21.0, 22.0, 22.0, 24.0, 25.0, 27.0, 28.0, 31.0, 33.0, 36.0, 41.0, 48.0]; + let n_series = if kind == ChartType::BoxWhisker { 2 } else { 1 }; + let categories = if kind == ChartType::BoxWhisker { + (0..obs.len()).map(|i| if i % 2 == 0 { "Group A" } else { "Group B" }.to_string()).collect() + } else { + vec![] + }; + let mut c = Chart { + kind, + categories, + gridlines: true, + legend: (kind == ChartType::BoxWhisker).then(|| "b".to_string()), + ..Default::default() + }; + for s in 0..n_series { + c.series.push(Series { + name: format!("Series {}", s + 1), + values: obs.iter().map(|v| Some(v + s as f64 * 6.0)).collect(), + ..Default::default() + }); + } + c + } + + fn sample_stock() -> Self { + let mut c = Chart { + kind: ChartType::Stock, + categories: ["Mon", "Tue", "Wed", "Thu", "Fri"].iter().map(ToString::to_string).collect(), + ..Default::default() + }; + let hlc = [("High", [55.0, 57.0, 57.0, 58.0, 58.0]), ("Low", [11.0, 12.0, 13.0, 11.0, 25.0]), ("Close", [25.0, 38.0, 50.0, 35.0, 43.0])]; + for (name, v) in hlc { + c.series.push(Series { name: name.into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() }); + } + c + } + /// The sample data a new chart starts with (our own numbers). pub fn sample(kind: ChartType) -> Self { - let cats = ["Q1", "Q2", "Q3", "Q4"]; - let data = [[4.2, 2.6, 2.1], [2.8, 4.1, 2.4], [3.6, 1.9, 3.3], [4.7, 3.0, 5.1]]; - let mut c = Chart { kind, categories: cats.iter().map(|s| s.to_string()).collect(), ..Default::default() }; if matches!(kind, ChartType::Treemap | ChartType::Sunburst) { - // A two-level hierarchy: months inside quarters. - c.categories = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"].iter().map(|s| s.to_string()).collect(); - c.category_levels = vec![["Q1", "Q1", "Q1", "Q2", "Q2", "Q2", "Q3", "Q3"].iter().map(|s| s.to_string()).collect()]; - let v = [4.2, 2.8, 3.6, 4.7, 2.6, 4.1, 1.9, 3.0]; - c.series.push(Series { name: "Series 1".into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() }); - c.vary_colors = true; - return c; + return Self::sample_treemap_sunburst(kind); } if matches!(kind, ChartType::Histogram | ChartType::Pareto | ChartType::BoxWhisker) { - // These plot raw observations: a histogram bins one series, a box chart summarises - // each category's points. - let obs = [12.0, 15.0, 17.0, 18.0, 21.0, 22.0, 22.0, 24.0, 25.0, 27.0, 28.0, 31.0, 33.0, 36.0, 41.0, 48.0]; - let n_series = if kind == ChartType::BoxWhisker { 2 } else { 1 }; - c.categories = if kind == ChartType::BoxWhisker { - (0..obs.len()).map(|i| if i % 2 == 0 { "Group A" } else { "Group B" }.to_string()).collect() - } else { - vec![] - }; - for s in 0..n_series { - c.series.push(Series { - name: format!("Series {}", s + 1), - values: obs.iter().map(|v| Some(v + s as f64 * 6.0)).collect(), - ..Default::default() - }); - } - c.gridlines = true; - c.legend = (kind == ChartType::BoxWhisker).then(|| "b".to_string()); - return c; + return Self::sample_observation_chart(kind); } if kind == ChartType::Stock { - // High-low-close over five days, the series in the order PowerPoint expects. - c.categories = ["Mon", "Tue", "Wed", "Thu", "Fri"].iter().map(|s| s.to_string()).collect(); - let hlc = [("High", [55.0, 57.0, 57.0, 58.0, 58.0]), ("Low", [11.0, 12.0, 13.0, 11.0, 25.0]), ("Close", [25.0, 38.0, 50.0, 35.0, 43.0])]; - for (name, v) in hlc { - c.series.push(Series { name: name.into(), values: v.iter().map(|x| Some(*x)).collect(), ..Default::default() }); - } - return c; + return Self::sample_stock(); } + let cats = ["Q1", "Q2", "Q3", "Q4"]; + let data = [[4.2, 2.6, 2.1], [2.8, 4.1, 2.4], [3.6, 1.9, 3.3], [4.7, 3.0, 5.1]]; + let mut c = Chart { kind, categories: cats.iter().map(ToString::to_string).collect(), ..Default::default() }; let n_series = if matches!(kind, ChartType::Pie | ChartType::Doughnut | ChartType::Funnel | ChartType::Waterfall | ChartType::Treemap) { 1 } else { 3 }; for s in 0..n_series { @@ -699,38 +720,52 @@ impl Chart { crate::numfmt::format(code.map_or("General", String::as_str), v, self.lang.as_deref()) } - /// Value range across series (stacked sums for stacked types). - pub fn value_range(&self) -> (f64, f64) { + fn waterfall_value_range(&self) -> (f64, f64) { let mut lo: f64 = 0.0; let mut hi: f64 = 0.0; - if self.kind == ChartType::Waterfall { - for (a, b) in self.waterfall_bars() { - lo = lo.min(a.min(b)); - hi = hi.max(a.max(b)); + for (a, b) in self.waterfall_bars() { + lo = lo.min(a.min(b)); + hi = hi.max(a.max(b)); + } + (lo, hi) + } + + fn stacked_value_range(&self, n: usize) -> (f64, f64) { + let mut lo: f64 = 0.0; + let mut hi: f64 = 0.0; + for i in 0..n { + let (mut p, mut m) = (0.0, 0.0); + for s in &self.series { + let v = s.values.get(i).copied().flatten().unwrap_or(0.0); + if v >= 0.0 { p += v } else { m += v } } - return (lo, hi); + hi = hi.max(p); + lo = lo.min(m); + } + (lo, hi) + } + + /// Value range across series (stacked sums for stacked types). + pub fn value_range(&self) -> (f64, f64) { + if self.kind == ChartType::Waterfall { + return self.waterfall_value_range(); } let n = self.categories.len().max(self.series.iter().map(|s| s.values.len()).max().unwrap_or(0)); - if self.kind.stacked() { - for i in 0..n { - let (mut p, mut m) = (0.0, 0.0); - for s in &self.series { - let v = s.values.get(i).copied().flatten().unwrap_or(0.0); - if v >= 0.0 { p += v } else { m += v } - } - hi = hi.max(p); - lo = lo.min(m); - } + let (mut lo, mut hi) = if self.kind.stacked() { + self.stacked_value_range(n) } else { + let mut l = 0.0; + let mut h = 0.0; for s in &self.series { for v in s.values.iter().flatten() { if v.is_finite() { - hi = hi.max(*v); - lo = lo.min(*v); + h = h.max(*v); + l = l.min(*v); } } } - } + (l, h) + }; if self.kind.percent() { return (0.0, 1.0); } diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index dd5d54e1..1289e47a 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -1066,13 +1066,13 @@ impl Presentation { self.slides.iter().position(|s| s.id == id) } pub fn slide(&self, id: SlideId) -> Option<&Slide> { - self.slides.iter().find(|s| s.id == id).map(|s| s.as_ref()) + self.slides.iter().find(|s| s.id == id).map(AsRef::as_ref) } pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> { self.slides.iter_mut().find(|s| s.id == id).map(Arc::make_mut) } pub fn slide_at(&self, i: usize) -> Option<&Slide> { - self.slides.get(i).map(|s| s.as_ref()) + self.slides.get(i).map(AsRef::as_ref) } /// Master and layout for a layout id. pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> { @@ -1086,7 +1086,7 @@ impl Presentation { self.masters.first().map(|m| (m.as_ref(), m.layouts.first())) } pub fn first_master(&self) -> Option<&Master> { - self.masters.first().map(|m| m.as_ref()) + self.masters.first().map(AsRef::as_ref) } pub fn media(&self, id: MediaId) -> Option<&MediaItem> { self.media.iter().find(|m| m.id == id) diff --git a/crates/model/src/style.rs b/crates/model/src/style.rs index b067c904..83c494bd 100644 --- a/crates/model/src/style.rs +++ b/crates/model/src/style.rs @@ -275,7 +275,7 @@ pub enum Dash { } impl Dash { - pub const PRESETS: [&'static str; 11] = + pub const PRESETS: [&str; 11] = ["solid", "sysDot", "sysDash", "dash", "dashDot", "lgDash", "lgDashDot", "lgDashDotDot", "dot", "sysDashDot", "sysDashDotDot"]; /// Dash/space lengths in multiples of the line width. pub fn pattern(&self) -> Vec { diff --git a/crates/model/src/text.rs b/crates/model/src/text.rs index 4129be4b..4b814129 100644 --- a/crates/model/src/text.rs +++ b/crates/model/src/text.rs @@ -530,7 +530,7 @@ impl Paragraph { true } pub fn text(&self) -> String { - self.runs.iter().map(|r| r.content()).collect() + self.runs.iter().map(Run::content).collect() } pub fn char_len(&self) -> usize { self.runs.iter().map(Run::char_len).sum() @@ -576,7 +576,7 @@ pub struct ListStyle { impl ListStyle { pub fn level(&self, l: u8) -> Option<&LevelStyle> { - self.levels.get(l as usize).and_then(|x| x.as_ref()) + self.levels.get(l as usize).and_then(Option::as_ref) } pub fn set(&mut self, l: u8, s: LevelStyle) { let i = (l as usize).min(8); diff --git a/crates/model/src/trendline.rs b/crates/model/src/trendline.rs index 05d792da..5ba71ff4 100644 --- a/crates/model/src/trendline.rs +++ b/crates/model/src/trendline.rs @@ -57,22 +57,7 @@ impl Trendline { let yhat: Vec = xs.iter().map(|x| poly_y(&coeff, *x)).collect(); (Model::Poly(coeff), ys, yhat) } - TrendlineType::Exp => { - let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(_, y)| *y > 0.0).collect(); - if pairs.len() < 2 { - return None; - } - let (xs, ln_y): (Vec, Vec) = pairs.iter().map(|(x, y)| (*x, y.ln())).unzip(); - let fixed = intercept.filter(|c| *c > 0.0).map(|c| c.ln()); - let (ln_c, b) = linreg(&xs, &ln_y, fixed)?; - let c = ln_c.exp(); - if !c.is_finite() || c <= 0.0 { - return None; - } - let ys: Vec = pairs.iter().map(|(_, y)| *y).collect(); - let yhat: Vec = xs.iter().map(|x| c * (b * x).exp()).collect(); - (Model::Exp { c, b }, ys, yhat) - } + TrendlineType::Exp => fit_exp(&pairs, intercept)?, TrendlineType::Log => { let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, _)| *x > 0.0).collect(); if pairs.len() < 2 { @@ -83,43 +68,10 @@ impl Trendline { let yhat: Vec = ln_x.iter().map(|u| b + c * u).collect(); (Model::Log { b, c }, ys, yhat) } - TrendlineType::Power => { - let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, y)| *x > 0.0 && *y > 0.0).collect(); - if pairs.len() < 2 { - return None; - } - let (ln_x, ln_y): (Vec, Vec) = pairs.iter().map(|(x, y)| (x.ln(), y.ln())).unzip(); - let fixed = intercept.filter(|c| *c > 0.0).map(|c| c.ln()); - let (ln_c, b) = linreg(&ln_x, &ln_y, fixed)?; - let c = ln_c.exp(); - if !c.is_finite() || c <= 0.0 { - return None; - } - let ys: Vec = pairs.iter().map(|(_, y)| *y).collect(); - let yhat: Vec = pairs.iter().map(|(x, _)| c * x.powf(b)).collect(); - (Model::Power { c, b }, ys, yhat) - } + TrendlineType::Power => fit_power(&pairs, intercept)?, TrendlineType::MovingAvg => { let period = self.period.unwrap_or(2).clamp(2, 255) as usize; - if pairs.len() <= period { - return None; - } - let mut pts = Vec::new(); - let mut ys = Vec::new(); - let mut yhat = Vec::new(); - for i in period - 1..pairs.len() { - let window = &pairs[i + 1 - period..=i]; - let avg = window.iter().map(|(_, y)| y).sum::() / period as f64; - if avg.is_finite() { - pts.push((pairs[i].0, avg)); - ys.push(pairs[i].1); - yhat.push(avg); - } - } - if pts.len() < 2 { - return None; - } - (Model::Moving(pts), ys, yhat) + fit_moving_avg(&pairs, period)? } }; if yhat.iter().any(|v| !v.is_finite()) { @@ -325,6 +277,89 @@ fn poly_y(coeff: &[f64], x: f64) -> f64 { acc } +fn fit_exp(pairs: &[(f64, f64)], intercept: Option) -> Option<(Model, Vec, Vec)> { + let pairs: Vec<(f64, f64)> = pairs.iter().copied().filter(|(_, y)| *y > 0.0).collect(); + if pairs.len() < 2 { + return None; + } + let (xs, ln_y): (Vec, Vec) = pairs.iter().map(|(x, y)| (*x, y.ln())).unzip(); + let fixed = intercept.filter(|c| *c > 0.0).map(f64::ln); + let (ln_c, b) = linreg(&xs, &ln_y, fixed)?; + let c = ln_c.exp(); + if !c.is_finite() || c <= 0.0 { + return None; + } + let ys: Vec = pairs.iter().map(|(_, y)| *y).collect(); + let yhat: Vec = xs.iter().map(|x| c * (b * x).exp()).collect(); + Some((Model::Exp { c, b }, ys, yhat)) +} + +fn fit_power(pairs: &[(f64, f64)], intercept: Option) -> Option<(Model, Vec, Vec)> { + let pairs: Vec<(f64, f64)> = pairs.iter().copied().filter(|(x, y)| *x > 0.0 && *y > 0.0).collect(); + if pairs.len() < 2 { + return None; + } + let (ln_x, ln_y): (Vec, Vec) = pairs.iter().map(|(x, y)| (x.ln(), y.ln())).unzip(); + let fixed = intercept.filter(|c| *c > 0.0).map(f64::ln); + let (ln_c, b) = linreg(&ln_x, &ln_y, fixed)?; + let c = ln_c.exp(); + if !c.is_finite() || c <= 0.0 { + return None; + } + let ys: Vec = pairs.iter().map(|(_, y)| *y).collect(); + let yhat: Vec = pairs.iter().map(|(x, _)| c * x.powf(b)).collect(); + Some((Model::Power { c, b }, ys, yhat)) +} + +fn fit_moving_avg(pairs: &[(f64, f64)], period: usize) -> Option<(Model, Vec, Vec)> { + if pairs.len() <= period { + return None; + } + let mut pts = Vec::new(); + let mut ys = Vec::new(); + let mut yhat = Vec::new(); + for i in period - 1..pairs.len() { + let window = &pairs[i + 1 - period..=i]; + let avg = window.iter().map(|(_, y)| y).sum::() / period as f64; + if avg.is_finite() { + pts.push((pairs[i].0, avg)); + ys.push(pairs[i].1); + yhat.push(avg); + } + } + if pts.len() < 2 { + return None; + } + Some((Model::Moving(pts), ys, yhat)) +} + +#[allow(clippy::needless_range_loop)] +fn polyfit_with_intercept(xs: &[f64], ys: &[f64], order: usize, b0: f64) -> Option> { + let m = order; + let mut ata = vec![vec![0.0; m]; m]; + let mut atb = vec![0.0; m]; + for (x, y) in xs.iter().zip(ys) { + let dy = y - b0; + for i in 0..m { + let xi = x.powi((i + 1) as i32); + atb[i] += dy * xi; + for j in i..m { + ata[i][j] += xi * x.powi((j + 1) as i32); + } + } + } + for i in 0..m { + for j in 0..i { + ata[i][j] = ata[j][i]; + } + } + let solved = solve(ata, atb)?; + let mut coeff = Vec::with_capacity(order + 1); + coeff.push(b0); + coeff.extend(solved); + coeff.iter().all(|v| v.is_finite()).then_some(coeff) +} + /// Polynomial through the normal equations. `x` is centred when the intercept is free, then /// expanded back to powers of `x`, so order 6 on category numbers stays solvable. #[allow(clippy::needless_range_loop)] // the index is the power in the normal equations @@ -334,29 +369,7 @@ fn polyfit(xs: &[f64], ys: &[f64], order: usize, intercept: Option) -> Opti return None; } if let Some(b0) = intercept { - let m = order; - let mut ata = vec![vec![0.0; m]; m]; - let mut atb = vec![0.0; m]; - for (x, y) in xs.iter().zip(ys) { - let dy = y - b0; - for i in 0..m { - let xi = x.powi((i + 1) as i32); - atb[i] += dy * xi; - for j in i..m { - ata[i][j] += xi * x.powi((j + 1) as i32); - } - } - } - for i in 0..m { - for j in 0..i { - ata[i][j] = ata[j][i]; - } - } - let solved = solve(ata, atb)?; - let mut coeff = Vec::with_capacity(order + 1); - coeff.push(b0); - coeff.extend(solved); - return coeff.iter().all(|v| v.is_finite()).then_some(coeff); + return polyfit_with_intercept(xs, ys, order, b0); } let xm = xs.iter().sum::() / n as f64; let us: Vec = xs.iter().map(|x| x - xm).collect(); diff --git a/crates/opus/src/celt/bands.rs b/crates/opus/src/celt/bands.rs index 74f19092..6d0bd3f0 100644 --- a/crates/opus/src/celt/bands.rs +++ b/crates/opus/src/celt/bands.rs @@ -71,7 +71,7 @@ fn exp_rotation1(x: &mut [f32], len: usize, stride: usize, c: f32, s: f32) { x[i + stride] = c * x2 + s * x1; x[i] = c * x1 + ms * x2; } - if len >= 2 * stride + 1 { + if len > 2 * stride { for i in (0..=len - 2 * stride - 1).rev() { let x1 = x[i]; let x2 = x[i + stride]; diff --git a/crates/opus/src/celt/mod.rs b/crates/opus/src/celt/mod.rs index 7110f5fb..bf2d75e7 100644 --- a/crates/opus/src/celt/mod.rs +++ b/crates/opus/src/celt/mod.rs @@ -578,7 +578,7 @@ fn tf_decode(start: usize, end: usize, is_transient: bool, tf_res: &mut [i32; NB let mut budget = dec.storage() as i32 * 8; let mut tell = dec.tell(); let mut logp: u32 = if is_transient { 2 } else { 4 }; - let tf_select_rsv = lm > 0 && tell + logp as i32 + 1 <= budget; + let tf_select_rsv = lm > 0 && tell + logp as i32 < budget; budget -= tf_select_rsv as i32; let mut tf_changed = 0; let mut curr = 0; diff --git a/crates/opus/src/decoder.rs b/crates/opus/src/decoder.rs index 573d5366..a9cdee29 100644 --- a/crates/opus/src/decoder.rs +++ b/crates/opus/src/decoder.rs @@ -217,7 +217,7 @@ impl StreamDecoder { } } } - let data_len = data.map_or(0, |d| d.len()); + let data_len = data.map_or(0, <[_]>::len); let mut dec_storage; let mut dec: Option<&mut RangeDecoder> = match data { Some(d) => { diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 066df561..40d09c71 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -131,6 +131,33 @@ pub fn export(pres: &Presentation, opts: &PdfOptions) -> Result> { write::guard(|| export_inner(pres, opts)) } +fn plan_pages(pres: &Presentation, opts: &PdfOptions, slides: &[usize], sw: f64, sh: f64) -> Vec> { + match opts.layout { + PageLayout::Slides => slides.iter().map(|&i| vec![(i, Rect::new(0.0, 0.0, sw, sh))]).collect(), + PageLayout::Notes => { + let b = notes_frames(pres, opts.paper).0; + slides.iter().map(|&i| vec![(i, b)]).collect() + } + PageLayout::Handouts { per_page } => { + let boxes = handout_boxes(per_page, opts.paper, sw / sh); + slides.chunks(boxes.len()).map(|c| c.iter().zip(&boxes).map(|(i, b)| (*i, *b)).collect()).collect() + } + } +} + +fn build_outline(pres: &Presentation, pages: &[Vec<(usize, Rect)>]) -> Outline { + let mut outline = Outline::new(); + for (p, v) in pages.iter().enumerate() { + for (i, _) in v { + let t = pres.slides[*i].title(); + let t = t.lines().next().unwrap_or("").trim().to_string(); + let label = if t.is_empty() { format!("Slide {}", i + 1) } else { format!("{}. {t}", i + 1) }; + outline.push_child(OutlineNode::new(label, XyzDestination::new(p, Point::from_xy(0.0, 0.0)))); + } + } + outline +} + fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result> { let slides: Vec = match &opts.slides { Some(v) => v.iter().copied().filter(|i| *i < pres.slides.len()).collect(), @@ -153,29 +180,10 @@ fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result> { let (sw, sh) = (pres.slide_size.width.max(1.0), pres.slide_size.height.max(1.0)); let mut ex = Exporter { pres, opts, fonts: HashMap::new() }; // Page plan: per page, the slides on it with their boxes (page points). - let pages: Vec> = match opts.layout { - PageLayout::Slides => slides.iter().map(|&i| vec![(i, Rect::new(0.0, 0.0, sw, sh))]).collect(), - PageLayout::Notes => { - let b = notes_frames(pres, opts.paper).0; - slides.iter().map(|&i| vec![(i, b)]).collect() - } - PageLayout::Handouts { per_page } => { - let boxes = handout_boxes(per_page, opts.paper, sw / sh); - slides.chunks(boxes.len()).map(|c| c.iter().zip(&boxes).map(|(i, b)| (*i, *b)).collect()).collect() - } - }; + let pages = plan_pages(pres, opts, &slides, sw, sh); // Bookmarks and slide → page lookup for internal links. let page_of: HashMap = pages.iter().enumerate().flat_map(|(p, v)| v.iter().map(move |(i, _)| (*i, p))).collect(); - let mut outline = Outline::new(); - for (p, v) in pages.iter().enumerate() { - for (i, _) in v { - let t = pres.slides[*i].title(); - let t = t.lines().next().unwrap_or("").trim().to_string(); - let label = if t.is_empty() { format!("Slide {}", i + 1) } else { format!("{}. {t}", i + 1) }; - outline.push_child(OutlineNode::new(label, XyzDestination::new(p, Point::from_xy(0.0, 0.0)))); - } - } - pdf.set_outline(outline); + pdf.set_outline(build_outline(pres, &pages)); for v in &pages { let (pw, ph) = match opts.layout { diff --git a/crates/render/src/images.rs b/crates/render/src/images.rs index 9bd6c421..135603b6 100644 --- a/crates/render/src/images.rs +++ b/crates/render/src/images.rs @@ -76,7 +76,7 @@ impl Anim { static CACHE: Mutex> = Mutex::new(None); fn with(f: impl FnOnce(&mut Cache) -> R) -> R { - let mut g = CACHE.lock().unwrap_or_else(|e| e.into_inner()); + let mut g = CACHE.lock().unwrap_or_else(std::sync::PoisonError::into_inner); f(g.get_or_insert_with(Cache::default)) } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index b78b96ea..6b17d004 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1090,7 +1090,7 @@ fn shape_item( for g in &glyphs { let Some(&h) = char_at_byte.get(&g.cluster) else { continue }; let Some(cell) = cells.get_mut(h) else { continue }; - if matches!(cell.ch, '\t' | '\u{b}' | '\n') { + if matches!(cell.ch, '\t'..='\u{b}') { continue; } let s = scale_of(cell); @@ -1738,7 +1738,7 @@ fn tab_segment(cells: &[Cell], i: usize) -> (f64, f64) { let mut w = 0.0; let mut dec = None; for c in cells.iter().skip(i + 1) { - if matches!(c.ch, '\t' | '\u{b}' | '\n') { + if matches!(c.ch, '\t'..='\u{b}') { break; } if c.ch == '.' && dec.is_none() { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 831da01f..efe8a07f 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2027,7 +2027,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { }); ui.horizontal(|ui| { if ui.button("+ Row").clicked() { - let n = grid.first().map(|r| r.len()).unwrap_or(2); + let n = grid.first().map(Vec::len).unwrap_or(2); grid.push(vec![String::new(); n]); } if ui.button("+ Series").clicked() { @@ -2943,7 +2943,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value { let line_value = finite_pt(&d.get("lineValue", "1")).unwrap_or(1.0); let before_same = d.get("spaceBefore", "") == d.get("spaceBeforeSeed", ""); let after_same = d.get("spaceAfter", "") == d.get("spaceAfterSeed", ""); - let v = json!({ + json!({ "align": d.get("align", "left"), "indentLeft": before, "indentRight": after, @@ -2957,8 +2957,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value { "lineValue": line_value, "tabs": d.params.get("tabs").cloned().unwrap_or_else(|| json!([])), "defaultTab": d.params.get("defaultTab").cloned().unwrap_or_else(|| json!(72.0)), - }); - v + }) } fn line_preset_label(kind: &str, value: &str) -> &'static str { diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index c963b663..340ca99f 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1349,6 +1349,38 @@ pub fn table_grid_picker(ui: &mut Ui) -> Option<(usize, usize)> { if resp.clicked() && hover.0 > 0 { Some(hover) } else { None } } +fn render_icon_categories(app: &mut SlideApp, ui: &mut Ui, icons: &[deckcraft_engine::icons::Icon], color: Color32, hover: Color32) { + let mut cats: Vec<&str> = Vec::new(); + for icon in icons { + if !cats.contains(&icon.category) { + cats.push(icon.category); + } + } + for cat in cats { + ui.label(egui::RichText::new(cat).small().strong()); + ui.horizontal_wrapped(|ui| { + for icon in icons.iter().filter(|i| i.category == cat) { + icon_cell(app, ui, icon, color, hover); + } + }); + } +} + +fn render_filtered_icons(app: &mut SlideApp, ui: &mut Ui, icons: &[deckcraft_engine::icons::Icon], q: &str, color: Color32, hover: Color32) { + let mut n = 0; + ui.horizontal_wrapped(|ui| { + for icon in icons { + if icon.name.contains(q) || icon.label.to_lowercase().contains(q) { + icon_cell(app, ui, icon, color, hover); + n += 1; + } + } + }); + if n == 0 { + ui.label("No icons match."); + } +} + /// Lucide stroke icons (Insert › Icons). Search filters the gallery; a click inserts the outline. fn icon_library(app: &mut SlideApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); @@ -1361,33 +1393,9 @@ fn icon_library(app: &mut SlideApp, ui: &mut Ui) { let icons = deckcraft_engine::icons::icons(); egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| { if q.is_empty() { - let mut cats: Vec<&str> = Vec::new(); - for icon in icons { - if !cats.contains(&icon.category) { - cats.push(icon.category); - } - } - for cat in cats { - ui.label(egui::RichText::new(cat).small().strong()); - ui.horizontal_wrapped(|ui| { - for icon in icons.iter().filter(|i| i.category == cat) { - icon_cell(app, ui, icon, color, hover); - } - }); - } + render_icon_categories(app, ui, icons, color, hover); } else { - let mut n = 0; - ui.horizontal_wrapped(|ui| { - for icon in icons { - if icon.name.contains(&q) || icon.label.to_lowercase().contains(&q) { - icon_cell(app, ui, icon, color, hover); - n += 1; - } - } - }); - if n == 0 { - ui.label("No icons match."); - } + render_filtered_icons(app, ui, icons, &q, color, hover); } }); } diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 7166924e..24d427ce 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -18,6 +18,7 @@ set -euo pipefail . "$(dirname "${BASH_SOURCE[0]}")/../common.sh" HERE="$ROOT/packaging/linux" APP_ID=ai.storyteller.deckcraft +CHECKSUM_FMT='%s %s\n' SKIP_BUILD=0 FORMATS="appimage deb rpm tar" @@ -137,21 +138,21 @@ if has appimage; then download="$TOOL.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" - printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - + printf "$CHECKSUM_FMT" "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - chmod 755 "$download" mv "$download" "$TOOL" fi - printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - + printf "$CHECKSUM_FMT" "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - fi RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" if [[ ! -f "$RUNTIME" ]]; then download="$RUNTIME.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" - printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c - + printf "$CHECKSUM_FMT" "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c - mv "$download" "$RUNTIME" fi - printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c - + printf "$CHECKSUM_FMT" "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c - # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be # relative). OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" diff --git a/xtask/src/assets.rs b/xtask/src/assets.rs index 6b37e557..a7169ee6 100644 --- a/xtask/src/assets.rs +++ b/xtask/src/assets.rs @@ -51,7 +51,7 @@ const ASSET_EXT: &[&str] = &[ const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "docs/brand/", "examples/"]; pub fn is_asset(path: &str) -> bool { - let ext = Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default(); + let ext = Path::new(path).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default(); ASSET_DIRS.iter().any(|d| path.starts_with(d)) || ASSET_EXT.contains(&ext.as_str()) } diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index b9bb409b..9921f9d7 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -207,7 +207,7 @@ pub fn check(crates: &[Crate]) -> Vec { } } } - out.sort_by_key(|v| v.to_string()); + out.sort_by_key(ToString::to_string); out.dedup(); out }