mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 02:27:08 -06:00
chore: resolve SonarCloud maintainability issues (#1255)
* chore: resolve SonarCloud maintainability issues * fix: address shellcheck SC2059 and fix generic cast ambiguity * style: apply rustfmt formatting * fix: restore compile and behaviour in Sonar refactors - fonts: char::is_alphanumeric takes &self, so filter needs a closure - h264: keep the mvd accumulator as i32 (prefix is usize) - model: annotate the non-stacked range accumulators as f64 - engine: chart.options still clears the raw chart XML for upDownBars and quartiles; smartart goes back to the glob import (cmd! macro) - web: keep the original viewport meta (behaviour change) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Assign positional parameters to locals in sha256 verification --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
This commit is contained in:
co-authored by
Claude Opus 5.5
Dylan Taylor
parent
b913c4f9d0
commit
5fadda5ecf
@@ -49,7 +49,7 @@ fn tokenize(s: &str) -> Vec<Tok> {
|
|||||||
n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit())
|
n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit())
|
||||||
} {
|
} {
|
||||||
i += 2;
|
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;
|
i += 1;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -2222,18 +2222,16 @@ fn neg_deinterleave(diff: i32, r: i32, max: i32) -> i32 {
|
|||||||
if diff <= 2 * r {
|
if diff <= 2 * r {
|
||||||
if diff & 1 == 1 {
|
if diff & 1 == 1 {
|
||||||
return r + ((diff + 1) >> 1);
|
return r + ((diff + 1) >> 1);
|
||||||
} else {
|
|
||||||
return r - (diff >> 1);
|
|
||||||
}
|
}
|
||||||
|
return r - (diff >> 1);
|
||||||
}
|
}
|
||||||
diff
|
diff
|
||||||
} else {
|
} else {
|
||||||
if diff <= 2 * (max - r - 1) {
|
if diff <= 2 * (max - r - 1) {
|
||||||
if diff & 1 == 1 {
|
if diff & 1 == 1 {
|
||||||
return r + ((diff + 1) >> 1);
|
return r + ((diff + 1) >> 1);
|
||||||
} else {
|
|
||||||
return r - (diff >> 1);
|
|
||||||
}
|
}
|
||||||
|
return r - (diff >> 1);
|
||||||
}
|
}
|
||||||
max - (diff + 1)
|
max - (diff + 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1761,6 +1761,39 @@ fn chart_data(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_bin_options(c: &mut deckcraft_model::chart::Chart, bin_count: Option<Option<u32>>, bin_size: Option<Option<f64>>) {
|
||||||
|
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<usize> = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
|
fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
// Histogram bins: a count or a width (setting one clears the other; null for automatic).
|
// 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));
|
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 +1805,8 @@ fn chart_options(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let subtotals: Option<Vec<usize>> =
|
let subtotals: Option<Vec<usize>> =
|
||||||
p.get("subtotals").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect());
|
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| {
|
with_chart(s, p, "chart.options", |c| {
|
||||||
if let Some(n) = bin_count {
|
apply_bin_options(c, bin_count, bin_size);
|
||||||
c.bin_count = n;
|
apply_subtotals_and_bars(c, p, subtotals.as_deref());
|
||||||
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<usize> = 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;
|
|
||||||
}
|
|
||||||
if bin_count.is_some() || bin_size.is_some() || subtotals.is_some() {
|
if bin_count.is_some() || bin_size.is_some() || subtotals.is_some() {
|
||||||
c.raw = None;
|
c.raw = None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -539,9 +539,9 @@ fn section_add(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
if doc.sections.is_empty() {
|
if doc.sections.is_empty() {
|
||||||
// The slides before `at` go into a "Default Section".
|
// The slides before `at` go into a "Default Section".
|
||||||
if at > 0 {
|
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 {
|
} else {
|
||||||
let Some(start) = ids.get(at).copied() else { return Err(bad("section.add", "no slide")) };
|
let Some(start) = ids.get(at).copied() else { return Err(bad("section.add", "no slide")) };
|
||||||
let si = doc.section_of(start).unwrap_or(0);
|
let si = doc.section_of(start).unwrap_or(0);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::{EngineError, Result, Session};
|
|||||||
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
|
||||||
payload
|
payload
|
||||||
.downcast_ref::<&str>()
|
.downcast_ref::<&str>()
|
||||||
.map(|s| s.to_string())
|
.map(ToString::to_string)
|
||||||
.or_else(|| payload.downcast_ref::<String>().cloned())
|
.or_else(|| payload.downcast_ref::<String>().cloned())
|
||||||
.unwrap_or_else(|| "unknown panic".into())
|
.unwrap_or_else(|| "unknown panic".into())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub fn stroke_width(icon: &Icon, w: f64, h: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parsed() -> &'static HashMap<&'static str, BezPath> {
|
fn parsed() -> &'static HashMap<&'static str, BezPath> {
|
||||||
static CACHE: OnceLock<HashMap<&'static str, BezPath>> = OnceLock::new();
|
static CACHE: OnceLock<HashMap<&str, BezPath>> = OnceLock::new();
|
||||||
CACHE.get_or_init(|| {
|
CACHE.get_or_init(|| {
|
||||||
let mut m = HashMap::new();
|
let mut m = HashMap::new();
|
||||||
for icon in ICONS {
|
for icon in ICONS {
|
||||||
|
|||||||
@@ -152,13 +152,13 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> {
|
|||||||
let Ok(rd) = std::fs::read_dir(dir) else { return out };
|
let Ok(rd) = std::fs::read_dir(dir) else { return out };
|
||||||
for e in rd.flatten() {
|
for e in rd.flatten() {
|
||||||
let p = e.path();
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if !std::fs::metadata(&p).is_ok_and(|m| m.is_file()) {
|
if !std::fs::metadata(&p).is_ok_and(|m| m.is_file()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<u64>().ok()) else { continue };
|
let Some(uid) = p.file_stem().and_then(std::ffi::OsStr::to_str).and_then(|s| s.parse::<u64>().ok()) else { continue };
|
||||||
// Metadata is a file on disk: anything but an object (corrupt, hand-edited) counts as none.
|
// 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)
|
let meta = read_regular(&p.with_extension("json"), MAX_META_BYTES)
|
||||||
.and_then(|t| serde_json::from_slice::<Value>(&t).ok())
|
.and_then(|t| serde_json::from_slice::<Value>(&t).ok())
|
||||||
|
|||||||
+28
-27
@@ -161,12 +161,12 @@ pub struct FaceRef(&'static FontFace);
|
|||||||
impl FaceRef {
|
impl FaceRef {
|
||||||
/// The handle for a loaded face (memoised per face).
|
/// The handle for a loaded face (memoised per face).
|
||||||
pub fn of(face: &Arc<FontFace>) -> FaceRef {
|
pub fn of(face: &Arc<FontFace>) -> FaceRef {
|
||||||
static LEAKED: RwLock<Vec<Option<&'static FontFace>>> = RwLock::new(Vec::new());
|
static LEAKED: RwLock<Vec<Option<&FontFace>>> = RwLock::new(Vec::new());
|
||||||
let id = face.id as usize;
|
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);
|
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 {
|
if w.len() <= id {
|
||||||
w.resize(id + 1, None);
|
w.resize(id + 1, None);
|
||||||
}
|
}
|
||||||
@@ -743,7 +743,7 @@ pub fn base_style(style: &str) -> &str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn norm(s: &str) -> String {
|
fn norm(s: &str) -> String {
|
||||||
s.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect()
|
s.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Weight implied by a style name.
|
/// Weight implied by a style name.
|
||||||
@@ -807,7 +807,7 @@ impl FontDb {
|
|||||||
pub fn set_system_fallback(&self, on: bool) {
|
pub fn set_system_fallback(&self, on: bool) {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let _ = on;
|
let _ = on;
|
||||||
@@ -822,14 +822,14 @@ impl FontDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
|
pub(crate) fn read_faces(&self) -> std::sync::RwLockReadGuard<'_, Vec<Arc<FontFace>>> {
|
||||||
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.
|
/// The system font catalog, scanned first if it hasn't been yet.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
|
fn read_catalog(&self) -> std::sync::RwLockReadGuard<'_, Vec<CatalogEntry>> {
|
||||||
self.ensure_catalog();
|
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.
|
/// 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;
|
let mut added = 0;
|
||||||
for f in new {
|
for f in new {
|
||||||
// Checked again under the lock: another thread may have added the same face.
|
// Checked again under the lock: another thread may have added the same face.
|
||||||
@@ -923,7 +923,7 @@ impl FontDb {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn load_system_fonts(&self) -> usize {
|
pub fn load_system_fonts(&self) -> usize {
|
||||||
// Families load again from the new catalog when next looked up.
|
// 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).
|
// The first scan, or a rescan once it is done (never both at once).
|
||||||
let mut first = None;
|
let mut first = None;
|
||||||
self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs()));
|
self.cataloged.get_or_init(|| first = Some(self.scan_font_dirs()));
|
||||||
@@ -959,7 +959,7 @@ impl FontDb {
|
|||||||
}
|
}
|
||||||
continue;
|
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")) {
|
if !matches!(ext.as_deref(), Some("ttf" | "otf" | "ttc" | "otc")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -975,7 +975,7 @@ impl FontDb {
|
|||||||
}
|
}
|
||||||
let n = found.len();
|
let n = found.len();
|
||||||
log::debug!("cataloged {n} system font faces");
|
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
|
n
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,7 +983,7 @@ impl FontDb {
|
|||||||
/// it is loaded.
|
/// it is loaded.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn load_cataloged(&self, family: &str) -> bool {
|
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)
|
self.load_family(family, &loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,7 +1047,7 @@ impl FontDb {
|
|||||||
// them rather than finding none and falling back to another family.
|
// them rather than finding none and falling back to another family.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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();
|
let key = family.to_lowercase();
|
||||||
if !loaded.contains(&key) {
|
if !loaded.contains(&key) {
|
||||||
self.load_cataloged(family);
|
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 = make_face(base.bytes.clone(), base.index, base.family.clone(), style.to_string(), coords)?;
|
||||||
let f = Arc::new(f);
|
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)
|
Some(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1214,20 +1214,20 @@ impl FontDb {
|
|||||||
}
|
}
|
||||||
let seen = self.read_faces().len();
|
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) {
|
if !sys.enabled || sys.misses.contains(&c) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c) && accept(f));
|
let covered = |db: &FontDb| db.read_faces().iter().any(|f| f.covers(c) && accept(f));
|
||||||
let cataloged: Vec<String> = self.read_catalog().iter().map(|e| e.family.clone()).collect();
|
let cataloged: Vec<String> = 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
|
// Another thread may have loaded a face for `c` (faces are only ever added), or found
|
||||||
// there is none, while this one waited.
|
// there is none, while this one waited.
|
||||||
if self.read_faces().len() != seen && covered(self) {
|
if self.read_faces().len() != seen && covered(self) {
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
for fam in SYSTEM_FALLBACKS {
|
for fam in SYSTEM_FALLBACKS {
|
||||||
@@ -1238,7 +1238,8 @@ impl FontDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut paths: Vec<std::path::PathBuf> = self.catalog.read().unwrap_or_else(|e| e.into_inner()).iter().map(|e| e.path.clone()).collect();
|
let mut paths: Vec<std::path::PathBuf> =
|
||||||
|
self.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner).iter().map(|e| e.path.clone()).collect();
|
||||||
paths.sort();
|
paths.sort();
|
||||||
paths.dedup();
|
paths.dedup();
|
||||||
for p in paths {
|
for p in paths {
|
||||||
@@ -1253,7 +1254,7 @@ impl FontDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if remember_miss {
|
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
|
false
|
||||||
}
|
}
|
||||||
@@ -1326,10 +1327,10 @@ impl FontDb {
|
|||||||
if c.is_control() || c.is_whitespace() {
|
if c.is_control() || c.is_whitespace() {
|
||||||
return false;
|
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;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
|
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
|
||||||
@@ -1347,7 +1348,7 @@ impl FontDb {
|
|||||||
return true;
|
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
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1359,12 +1360,12 @@ impl FontDb {
|
|||||||
return false;
|
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 {
|
if !sys.enabled {
|
||||||
return false;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
|
let mut paths: Vec<std::path::PathBuf> = self.read_catalog().iter().map(|e| e.path.clone()).collect();
|
||||||
@@ -1382,14 +1383,14 @@ impl FontDb {
|
|||||||
return true;
|
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
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Glyph outline in font units, y-down (flipped), cached per (face, glyph).
|
/// Glyph outline in font units, y-down (flipped), cached per (face, glyph).
|
||||||
pub fn outline(&self, face: &FontFace, gid: u32) -> Arc<BezPath> {
|
pub fn outline(&self, face: &FontFace, gid: u32) -> Arc<BezPath> {
|
||||||
let key = (face.id, gid);
|
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();
|
return p.clone();
|
||||||
}
|
}
|
||||||
let mut pen = FlipPen(BezPath::new());
|
let mut pen = FlipPen(BezPath::new());
|
||||||
@@ -1399,7 +1400,7 @@ impl FontDb {
|
|||||||
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen);
|
let _ = g.draw(DrawSettings::unhinted(Size::unscaled(), face.location()), &mut pen);
|
||||||
}
|
}
|
||||||
let p = Arc::new(pen.0);
|
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 {
|
if cache.len() >= OUTLINE_CACHE_MAX {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-26
@@ -1070,33 +1070,33 @@ mod tests {
|
|||||||
assert_eq!(md_list_item(not), None, "{not:?}");
|
assert_eq!(md_list_item(not), None, "{not:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// #814: slides_to_outline includes table cell text, which it used to skip.
|
/// #814: slides_to_outline includes table cell text, which it used to skip.
|
||||||
#[test]
|
#[test]
|
||||||
fn outline_includes_table_text() {
|
fn outline_includes_table_text() {
|
||||||
use deckcraft_model::table::{Cell, Row, Table};
|
use deckcraft_model::table::{Cell, Row, Table};
|
||||||
use deckcraft_model::{Shape, ShapeKind, Xfrm};
|
use deckcraft_model::{Shape, ShapeKind, Xfrm};
|
||||||
let mut p = defaults::new_presentation(None);
|
let mut p = defaults::new_presentation(None);
|
||||||
let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap();
|
let layout = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap();
|
||||||
let mut s = defaults::new_slide(&mut p, layout).expect("slide");
|
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()));
|
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() };
|
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"));
|
body.text = Some(TextBody::from_text("Text box body"));
|
||||||
s.shapes.push(body);
|
s.shapes.push(body);
|
||||||
|
|
||||||
let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() };
|
let mut table = Table { cols: vec![200.0, 200.0], ..Default::default() };
|
||||||
for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] {
|
for texts in [["Cell A1", "Cell B1"], ["Cell A2", "Cell B2"]] {
|
||||||
let cells: Vec<Cell> = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect();
|
let cells: Vec<Cell> = texts.iter().map(|t| Cell { text: TextBody::from_text(t), ..Default::default() }).collect();
|
||||||
table.rows.push(Row { height: 30.0, cells });
|
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}");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ impl Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn with<R>(&self, f: impl FnOnce(&mut Mixer) -> R) -> R {
|
fn with<R>(&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)
|
f(&mut g)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1187,7 +1187,7 @@ fn image(r: Bytes) -> Option<ImageData> {
|
|||||||
fn metafile(r: Bytes) -> Option<Picture> {
|
fn metafile(r: Bytes) -> Option<Picture> {
|
||||||
let size = r.u32(12)? as usize;
|
let size = r.u32(12)? as usize;
|
||||||
let data = r.slice(16, size).or_else(|| r.tail(16).map(|t| t.0))?;
|
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 {
|
if depth >= MAX_NESTING {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1716,7 +1716,7 @@ fn plus_pic(recs: Vec<Vec<u8>>) -> Picture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn u16s(s: &str) -> Vec<u8> {
|
fn u16s(s: &str) -> Vec<u8> {
|
||||||
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.
|
/// A 2 x 2 32bpp ARGB bitmap image object: red, green / blue, white.
|
||||||
|
|||||||
+86
-56
@@ -491,50 +491,66 @@ impl Default for Chart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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).
|
/// The sample data a new chart starts with (our own numbers).
|
||||||
pub fn sample(kind: ChartType) -> Self {
|
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) {
|
if matches!(kind, ChartType::Treemap | ChartType::Sunburst) {
|
||||||
// A two-level hierarchy: months inside quarters.
|
return Self::sample_treemap_sunburst(kind);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
if matches!(kind, ChartType::Histogram | ChartType::Pareto | ChartType::BoxWhisker) {
|
if matches!(kind, ChartType::Histogram | ChartType::Pareto | ChartType::BoxWhisker) {
|
||||||
// These plot raw observations: a histogram bins one series, a box chart summarises
|
return Self::sample_observation_chart(kind);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
if kind == ChartType::Stock {
|
if kind == ChartType::Stock {
|
||||||
// High-low-close over five days, the series in the order PowerPoint expects.
|
return Self::sample_stock();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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 =
|
let n_series =
|
||||||
if matches!(kind, ChartType::Pie | ChartType::Doughnut | ChartType::Funnel | ChartType::Waterfall | ChartType::Treemap) { 1 } else { 3 };
|
if matches!(kind, ChartType::Pie | ChartType::Doughnut | ChartType::Funnel | ChartType::Waterfall | ChartType::Treemap) { 1 } else { 3 };
|
||||||
for s in 0..n_series {
|
for s in 0..n_series {
|
||||||
@@ -699,38 +715,52 @@ impl Chart {
|
|||||||
crate::numfmt::format(code.map_or("General", String::as_str), v, self.lang.as_deref())
|
crate::numfmt::format(code.map_or("General", String::as_str), v, self.lang.as_deref())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Value range across series (stacked sums for stacked types).
|
fn waterfall_value_range(&self) -> (f64, f64) {
|
||||||
pub fn value_range(&self) -> (f64, f64) {
|
|
||||||
let mut lo: f64 = 0.0;
|
let mut lo: f64 = 0.0;
|
||||||
let mut hi: f64 = 0.0;
|
let mut hi: f64 = 0.0;
|
||||||
if self.kind == ChartType::Waterfall {
|
for (a, b) in self.waterfall_bars() {
|
||||||
for (a, b) in self.waterfall_bars() {
|
lo = lo.min(a.min(b));
|
||||||
lo = lo.min(a.min(b));
|
hi = hi.max(a.max(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));
|
let n = self.categories.len().max(self.series.iter().map(|s| s.values.len()).max().unwrap_or(0));
|
||||||
if self.kind.stacked() {
|
let (mut lo, mut hi) = if self.kind.stacked() {
|
||||||
for i in 0..n {
|
self.stacked_value_range(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);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
let mut l: f64 = 0.0;
|
||||||
|
let mut h: f64 = 0.0;
|
||||||
for s in &self.series {
|
for s in &self.series {
|
||||||
for v in s.values.iter().flatten() {
|
for v in s.values.iter().flatten() {
|
||||||
if v.is_finite() {
|
if v.is_finite() {
|
||||||
hi = hi.max(*v);
|
h = h.max(*v);
|
||||||
lo = lo.min(*v);
|
l = l.min(*v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
(l, h)
|
||||||
|
};
|
||||||
if self.kind.percent() {
|
if self.kind.percent() {
|
||||||
return (0.0, 1.0);
|
return (0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1066,13 +1066,13 @@ impl Presentation {
|
|||||||
self.slides.iter().position(|s| s.id == id)
|
self.slides.iter().position(|s| s.id == id)
|
||||||
}
|
}
|
||||||
pub fn slide(&self, id: SlideId) -> Option<&Slide> {
|
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> {
|
pub fn slide_mut(&mut self, id: SlideId) -> Option<&mut Slide> {
|
||||||
self.slides.iter_mut().find(|s| s.id == id).map(Arc::make_mut)
|
self.slides.iter_mut().find(|s| s.id == id).map(Arc::make_mut)
|
||||||
}
|
}
|
||||||
pub fn slide_at(&self, i: usize) -> Option<&Slide> {
|
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.
|
/// Master and layout for a layout id.
|
||||||
pub fn layout(&self, id: LayoutId) -> Option<(&Master, &Layout)> {
|
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()))
|
self.masters.first().map(|m| (m.as_ref(), m.layouts.first()))
|
||||||
}
|
}
|
||||||
pub fn first_master(&self) -> Option<&Master> {
|
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> {
|
pub fn media(&self, id: MediaId) -> Option<&MediaItem> {
|
||||||
self.media.iter().find(|m| m.id == id)
|
self.media.iter().find(|m| m.id == id)
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ pub enum Dash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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"];
|
["solid", "sysDot", "sysDash", "dash", "dashDot", "lgDash", "lgDashDot", "lgDashDotDot", "dot", "sysDashDot", "sysDashDotDot"];
|
||||||
/// Dash/space lengths in multiples of the line width.
|
/// Dash/space lengths in multiples of the line width.
|
||||||
pub fn pattern(&self) -> Vec<f64> {
|
pub fn pattern(&self) -> Vec<f64> {
|
||||||
|
|||||||
@@ -530,7 +530,7 @@ impl Paragraph {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
pub fn text(&self) -> String {
|
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 {
|
pub fn char_len(&self) -> usize {
|
||||||
self.runs.iter().map(Run::char_len).sum()
|
self.runs.iter().map(Run::char_len).sum()
|
||||||
@@ -576,7 +576,7 @@ pub struct ListStyle {
|
|||||||
|
|
||||||
impl ListStyle {
|
impl ListStyle {
|
||||||
pub fn level(&self, l: u8) -> Option<&LevelStyle> {
|
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) {
|
pub fn set(&mut self, l: u8, s: LevelStyle) {
|
||||||
let i = (l as usize).min(8);
|
let i = (l as usize).min(8);
|
||||||
|
|||||||
@@ -57,22 +57,7 @@ impl Trendline {
|
|||||||
let yhat: Vec<f64> = xs.iter().map(|x| poly_y(&coeff, *x)).collect();
|
let yhat: Vec<f64> = xs.iter().map(|x| poly_y(&coeff, *x)).collect();
|
||||||
(Model::Poly(coeff), ys, yhat)
|
(Model::Poly(coeff), ys, yhat)
|
||||||
}
|
}
|
||||||
TrendlineType::Exp => {
|
TrendlineType::Exp => fit_exp(&pairs, intercept)?,
|
||||||
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<f64>, Vec<f64>) = 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<f64> = pairs.iter().map(|(_, y)| *y).collect();
|
|
||||||
let yhat: Vec<f64> = xs.iter().map(|x| c * (b * x).exp()).collect();
|
|
||||||
(Model::Exp { c, b }, ys, yhat)
|
|
||||||
}
|
|
||||||
TrendlineType::Log => {
|
TrendlineType::Log => {
|
||||||
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, _)| *x > 0.0).collect();
|
let pairs: Vec<(f64, f64)> = pairs.into_iter().filter(|(x, _)| *x > 0.0).collect();
|
||||||
if pairs.len() < 2 {
|
if pairs.len() < 2 {
|
||||||
@@ -83,43 +68,10 @@ impl Trendline {
|
|||||||
let yhat: Vec<f64> = ln_x.iter().map(|u| b + c * u).collect();
|
let yhat: Vec<f64> = ln_x.iter().map(|u| b + c * u).collect();
|
||||||
(Model::Log { b, c }, ys, yhat)
|
(Model::Log { b, c }, ys, yhat)
|
||||||
}
|
}
|
||||||
TrendlineType::Power => {
|
TrendlineType::Power => fit_power(&pairs, intercept)?,
|
||||||
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<f64>, Vec<f64>) = 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<f64> = pairs.iter().map(|(_, y)| *y).collect();
|
|
||||||
let yhat: Vec<f64> = pairs.iter().map(|(x, _)| c * x.powf(b)).collect();
|
|
||||||
(Model::Power { c, b }, ys, yhat)
|
|
||||||
}
|
|
||||||
TrendlineType::MovingAvg => {
|
TrendlineType::MovingAvg => {
|
||||||
let period = self.period.unwrap_or(2).clamp(2, 255) as usize;
|
let period = self.period.unwrap_or(2).clamp(2, 255) as usize;
|
||||||
if pairs.len() <= period {
|
fit_moving_avg(&pairs, 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::<f64>() / 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)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if yhat.iter().any(|v| !v.is_finite()) {
|
if yhat.iter().any(|v| !v.is_finite()) {
|
||||||
@@ -325,6 +277,89 @@ fn poly_y(coeff: &[f64], x: f64) -> f64 {
|
|||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fit_exp(pairs: &[(f64, f64)], intercept: Option<f64>) -> Option<(Model, Vec<f64>, Vec<f64>)> {
|
||||||
|
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<f64>, Vec<f64>) = 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<f64> = pairs.iter().map(|(_, y)| *y).collect();
|
||||||
|
let yhat: Vec<f64> = xs.iter().map(|x| c * (b * x).exp()).collect();
|
||||||
|
Some((Model::Exp { c, b }, ys, yhat))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fit_power(pairs: &[(f64, f64)], intercept: Option<f64>) -> Option<(Model, Vec<f64>, Vec<f64>)> {
|
||||||
|
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<f64>, Vec<f64>) = 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<f64> = pairs.iter().map(|(_, y)| *y).collect();
|
||||||
|
let yhat: Vec<f64> = 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<f64>, Vec<f64>)> {
|
||||||
|
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::<f64>() / 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<Vec<f64>> {
|
||||||
|
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
|
/// 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.
|
/// 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
|
#[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<f64>) -> Opti
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if let Some(b0) = intercept {
|
if let Some(b0) = intercept {
|
||||||
let m = order;
|
return polyfit_with_intercept(xs, ys, order, b0);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
let xm = xs.iter().sum::<f64>() / n as f64;
|
let xm = xs.iter().sum::<f64>() / n as f64;
|
||||||
let us: Vec<f64> = xs.iter().map(|x| x - xm).collect();
|
let us: Vec<f64> = xs.iter().map(|x| x - xm).collect();
|
||||||
|
|||||||
@@ -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 + stride] = c * x2 + s * x1;
|
||||||
x[i] = c * x1 + ms * x2;
|
x[i] = c * x1 + ms * x2;
|
||||||
}
|
}
|
||||||
if len >= 2 * stride + 1 {
|
if len > 2 * stride {
|
||||||
for i in (0..=len - 2 * stride - 1).rev() {
|
for i in (0..=len - 2 * stride - 1).rev() {
|
||||||
let x1 = x[i];
|
let x1 = x[i];
|
||||||
let x2 = x[i + stride];
|
let x2 = x[i + stride];
|
||||||
|
|||||||
@@ -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 budget = dec.storage() as i32 * 8;
|
||||||
let mut tell = dec.tell();
|
let mut tell = dec.tell();
|
||||||
let mut logp: u32 = if is_transient { 2 } else { 4 };
|
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;
|
budget -= tf_select_rsv as i32;
|
||||||
let mut tf_changed = 0;
|
let mut tf_changed = 0;
|
||||||
let mut curr = 0;
|
let mut curr = 0;
|
||||||
|
|||||||
@@ -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_storage;
|
||||||
let mut dec: Option<&mut RangeDecoder> = match data {
|
let mut dec: Option<&mut RangeDecoder> = match data {
|
||||||
Some(d) => {
|
Some(d) => {
|
||||||
|
|||||||
+29
-21
@@ -131,6 +131,33 @@ pub fn export(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
|
|||||||
write::guard(|| export_inner(pres, opts))
|
write::guard(|| export_inner(pres, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn plan_pages(pres: &Presentation, opts: &PdfOptions, slides: &[usize], sw: f64, sh: f64) -> Vec<Vec<(usize, Rect)>> {
|
||||||
|
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<Vec<u8>> {
|
fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
|
||||||
let slides: Vec<usize> = match &opts.slides {
|
let slides: Vec<usize> = match &opts.slides {
|
||||||
Some(v) => v.iter().copied().filter(|i| *i < pres.slides.len()).collect(),
|
Some(v) => v.iter().copied().filter(|i| *i < pres.slides.len()).collect(),
|
||||||
@@ -153,29 +180,10 @@ fn export_inner(pres: &Presentation, opts: &PdfOptions) -> Result<Vec<u8>> {
|
|||||||
let (sw, sh) = (pres.slide_size.width.max(1.0), pres.slide_size.height.max(1.0));
|
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() };
|
let mut ex = Exporter { pres, opts, fonts: HashMap::new() };
|
||||||
// Page plan: per page, the slides on it with their boxes (page points).
|
// Page plan: per page, the slides on it with their boxes (page points).
|
||||||
let pages: Vec<Vec<(usize, Rect)>> = match opts.layout {
|
let pages = plan_pages(pres, opts, &slides, sw, sh);
|
||||||
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()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Bookmarks and slide → page lookup for internal links.
|
// Bookmarks and slide → page lookup for internal links.
|
||||||
let page_of: HashMap<usize, usize> = pages.iter().enumerate().flat_map(|(p, v)| v.iter().map(move |(i, _)| (*i, p))).collect();
|
let page_of: HashMap<usize, usize> = pages.iter().enumerate().flat_map(|(p, v)| v.iter().map(move |(i, _)| (*i, p))).collect();
|
||||||
let mut outline = Outline::new();
|
pdf.set_outline(build_outline(pres, &pages));
|
||||||
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);
|
|
||||||
|
|
||||||
for v in &pages {
|
for v in &pages {
|
||||||
let (pw, ph) = match opts.layout {
|
let (pw, ph) = match opts.layout {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl Anim {
|
|||||||
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
|
static CACHE: Mutex<Option<Cache>> = Mutex::new(None);
|
||||||
|
|
||||||
fn with<R>(f: impl FnOnce(&mut Cache) -> R) -> R {
|
fn with<R>(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))
|
f(g.get_or_insert_with(Cache::default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1090,7 +1090,7 @@ fn shape_item(
|
|||||||
for g in &glyphs {
|
for g in &glyphs {
|
||||||
let Some(&h) = char_at_byte.get(&g.cluster) else { continue };
|
let Some(&h) = char_at_byte.get(&g.cluster) else { continue };
|
||||||
let Some(cell) = cells.get_mut(h) 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;
|
continue;
|
||||||
}
|
}
|
||||||
let s = scale_of(cell);
|
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 w = 0.0;
|
||||||
let mut dec = None;
|
let mut dec = None;
|
||||||
for c in cells.iter().skip(i + 1) {
|
for c in cells.iter().skip(i + 1) {
|
||||||
if matches!(c.ch, '\t' | '\u{b}' | '\n') {
|
if matches!(c.ch, '\t'..='\u{b}') {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if c.ch == '.' && dec.is_none() {
|
if c.ch == '.' && dec.is_none() {
|
||||||
|
|||||||
@@ -2027,7 +2027,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
|||||||
});
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("+ Row").clicked() {
|
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]);
|
grid.push(vec![String::new(); n]);
|
||||||
}
|
}
|
||||||
if ui.button("+ Series").clicked() {
|
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 line_value = finite_pt(&d.get("lineValue", "1")).unwrap_or(1.0);
|
||||||
let before_same = d.get("spaceBefore", "") == d.get("spaceBeforeSeed", "");
|
let before_same = d.get("spaceBefore", "") == d.get("spaceBeforeSeed", "");
|
||||||
let after_same = d.get("spaceAfter", "") == d.get("spaceAfterSeed", "");
|
let after_same = d.get("spaceAfter", "") == d.get("spaceAfterSeed", "");
|
||||||
let v = json!({
|
json!({
|
||||||
"align": d.get("align", "left"),
|
"align": d.get("align", "left"),
|
||||||
"indentLeft": before,
|
"indentLeft": before,
|
||||||
"indentRight": after,
|
"indentRight": after,
|
||||||
@@ -2957,8 +2957,7 @@ fn paragraph_return_params(d: &mut Dialog) -> Value {
|
|||||||
"lineValue": line_value,
|
"lineValue": line_value,
|
||||||
"tabs": d.params.get("tabs").cloned().unwrap_or_else(|| json!([])),
|
"tabs": d.params.get("tabs").cloned().unwrap_or_else(|| json!([])),
|
||||||
"defaultTab": d.params.get("defaultTab").cloned().unwrap_or_else(|| json!(72.0)),
|
"defaultTab": d.params.get("defaultTab").cloned().unwrap_or_else(|| json!(72.0)),
|
||||||
});
|
})
|
||||||
v
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_preset_label(kind: &str, value: &str) -> &'static str {
|
fn line_preset_label(kind: &str, value: &str) -> &'static str {
|
||||||
|
|||||||
@@ -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 }
|
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.
|
/// Lucide stroke icons (Insert › Icons). Search filters the gallery; a click inserts the outline.
|
||||||
fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
|
fn icon_library(app: &mut SlideApp, ui: &mut Ui) {
|
||||||
let t = Tokens::get(ui.ctx());
|
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();
|
let icons = deckcraft_engine::icons::icons();
|
||||||
egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| {
|
egui::ScrollArea::vertical().id_salt("icon-scroll").max_height(420.0).show(ui, |ui| {
|
||||||
if q.is_empty() {
|
if q.is_empty() {
|
||||||
let mut cats: Vec<&str> = Vec::new();
|
render_icon_categories(app, ui, icons, color, hover);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let mut n = 0;
|
render_filtered_icons(app, ui, icons, &q, color, hover);
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ set -euo pipefail
|
|||||||
HERE="$ROOT/packaging/linux"
|
HERE="$ROOT/packaging/linux"
|
||||||
APP_ID=ai.storyteller.deckcraft
|
APP_ID=ai.storyteller.deckcraft
|
||||||
|
|
||||||
|
verify_sha256() {
|
||||||
|
local expected="$1"
|
||||||
|
local file="$2"
|
||||||
|
printf '%s %s\n' "$expected" "$file" | sha256sum -c -
|
||||||
|
}
|
||||||
|
|
||||||
SKIP_BUILD=0
|
SKIP_BUILD=0
|
||||||
FORMATS="appimage deb rpm tar"
|
FORMATS="appimage deb rpm tar"
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
@@ -137,21 +143,21 @@ if has appimage; then
|
|||||||
download="$TOOL.download"
|
download="$TOOL.download"
|
||||||
curl --proto '=https' -fsSL -o "$download" \
|
curl --proto '=https' -fsSL -o "$download" \
|
||||||
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
|
||||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
|
verify_sha256 "$APPIMAGETOOL_SHA256" "$download"
|
||||||
chmod 755 "$download"
|
chmod 755 "$download"
|
||||||
mv "$download" "$TOOL"
|
mv "$download" "$TOOL"
|
||||||
fi
|
fi
|
||||||
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
|
verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
|
||||||
fi
|
fi
|
||||||
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
|
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
|
||||||
if [[ ! -f "$RUNTIME" ]]; then
|
if [[ ! -f "$RUNTIME" ]]; then
|
||||||
download="$RUNTIME.download"
|
download="$RUNTIME.download"
|
||||||
curl --proto '=https' -fsSL -o "$download" \
|
curl --proto '=https' -fsSL -o "$download" \
|
||||||
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
|
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
|
||||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c -
|
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download"
|
||||||
mv "$download" "$RUNTIME"
|
mv "$download" "$RUNTIME"
|
||||||
fi
|
fi
|
||||||
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
|
verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME"
|
||||||
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
|
||||||
# relative).
|
# relative).
|
||||||
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ const ASSET_EXT: &[&str] = &[
|
|||||||
const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "docs/brand/", "examples/"];
|
const ASSET_DIRS: &[&str] = &["assets/", "docs/images/", "docs/brand/", "examples/"];
|
||||||
|
|
||||||
pub fn is_asset(path: &str) -> bool {
|
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())
|
ASSET_DIRS.iter().any(|d| path.starts_with(d)) || ASSET_EXT.contains(&ext.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -207,7 +207,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.sort_by_key(|v| v.to_string());
|
out.sort_by_key(ToString::to_string);
|
||||||
out.dedup();
|
out.dedup();
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user