mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 03:36:54 -06:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50ea1fe080 | ||
|
|
0e7456c0d3 | ||
|
|
6401899ebc | ||
|
|
5b2921e616 | ||
|
|
9f22a06627 | ||
|
|
0ad5d7d6e0 | ||
|
|
f77737b3ef | ||
|
|
a12e1e3296 | ||
|
|
f3d84d4119 | ||
|
|
b622394468 | ||
|
|
fb69da0068 | ||
|
|
b8d4cd5161 | ||
|
|
896b7f8e05 | ||
|
|
4e86fe7ad3 |
@@ -318,7 +318,7 @@ fn walk(dir: &Path, depth: usize, out: &mut Discovery, seen: &mut std::collectio
|
|||||||
if !walk(&p, depth + 1, out, seen) {
|
if !walk(&p, depth + 1, out, seen) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("xlsx") || e.eq_ignore_ascii_case("xlsm"))
|
} else if p.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("xlsx") || e.eq_ignore_ascii_case("xlsm"))
|
||||||
&& !out.add_file(p)
|
&& !out.add_file(p)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -333,7 +333,7 @@ fn walk(dir: &Path, depth: usize, out: &mut Discovery, seen: &mut std::collectio
|
|||||||
pub fn check_bytes(path: &str, bytes: &[u8], opts: &Options) -> FileReport {
|
pub fn check_bytes(path: &str, bytes: &[u8], opts: &Options) -> FileReport {
|
||||||
let guarded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check_inner(path, bytes, opts)));
|
let guarded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check_inner(path, bytes, opts)));
|
||||||
guarded.unwrap_or_else(|p| {
|
guarded.unwrap_or_else(|p| {
|
||||||
let msg = p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
let msg = p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
||||||
FileReport { path: path.to_string(), error: Some(format!("internal error while checking: {msg}")), ..Default::default() }
|
FileReport { path: path.to_string(), error: Some(format!("internal error while checking: {msg}")), ..Default::default() }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ fn download(name: &str, bytes: &[u8]) -> Result<(), String> {
|
|||||||
js_sys::Reflect::get(&window, &JsValue::from_str("showSaveFilePicker")).ok().and_then(|v| v.dyn_into().ok());
|
js_sys::Reflect::get(&window, &JsValue::from_str("showSaveFilePicker")).ok().and_then(|v| v.dyn_into().ok());
|
||||||
|
|
||||||
if let Some(picker_fn) = show_picker_fn {
|
if let Some(picker_fn) = show_picker_fn {
|
||||||
let ext = std::path::Path::new(name).extension().and_then(|e| e.to_str()).unwrap_or("xlsx").to_ascii_lowercase();
|
let ext = std::path::Path::new(name).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("xlsx").to_ascii_lowercase();
|
||||||
let dot_ext = format!(".{ext}");
|
let dot_ext = format!(".{ext}");
|
||||||
let mime = mime_for(name);
|
let mime = mime_for(name);
|
||||||
|
|
||||||
|
|||||||
@@ -180,11 +180,11 @@ fn services() -> Services {
|
|||||||
})),
|
})),
|
||||||
pick_save: Some(Box::new(|suggested: &str| {
|
pick_save: Some(Box::new(|suggested: &str| {
|
||||||
let path = std::path::Path::new(suggested);
|
let path = std::path::Path::new(suggested);
|
||||||
let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("Book1").to_string();
|
let stem = path.file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Book1").to_string();
|
||||||
// The suggested name's type (an export asks for .pdf, .csv or .html) comes first.
|
// The suggested name's type (an export asks for .pdf, .csv or .html) comes first.
|
||||||
const TYPES: [(&str, &str); 4] =
|
const TYPES: [(&str, &str); 4] =
|
||||||
[("Excel Workbook (.xlsx)", "xlsx"), ("CSV UTF-8 (.csv)", "csv"), ("Web Page (.html)", "html"), ("PDF (.pdf)", "pdf")];
|
[("Excel Workbook (.xlsx)", "xlsx"), ("CSV UTF-8 (.csv)", "csv"), ("Web Page (.html)", "html"), ("PDF (.pdf)", "pdf")];
|
||||||
let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).filter(|e| TYPES.iter().any(|(_, x)| x == e));
|
let ext = path.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).filter(|e| TYPES.iter().any(|(_, x)| x == e));
|
||||||
let ext = ext.unwrap_or_else(|| "xlsx".to_string());
|
let ext = ext.unwrap_or_else(|| "xlsx".to_string());
|
||||||
let mut dialog = rfd::FileDialog::new().set_file_name(format!("{stem}.{ext}"));
|
let mut dialog = rfd::FileDialog::new().set_file_name(format!("{stem}.{ext}"));
|
||||||
for (label, x) in TYPES.iter().filter(|(_, x)| *x == ext).chain(TYPES.iter().filter(|(_, x)| *x != ext)) {
|
for (label, x) in TYPES.iter().filter(|(_, x)| *x == ext).chain(TYPES.iter().filter(|(_, x)| *x != ext)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub fn discover() -> Discover {
|
|||||||
static JOBS: std::sync::Mutex<Vec<std::thread::JoinHandle<()>>> = std::sync::Mutex::new(Vec::new());
|
static JOBS: std::sync::Mutex<Vec<std::thread::JoinHandle<()>>> = std::sync::Mutex::new(Vec::new());
|
||||||
|
|
||||||
pub fn finish_pending_jobs() {
|
pub fn finish_pending_jobs() {
|
||||||
let workers = std::mem::take(&mut *JOBS.lock().unwrap_or_else(|poison| poison.into_inner()));
|
let workers = std::mem::take(&mut *JOBS.lock().unwrap_or_else(std::sync::PoisonError::into_inner));
|
||||||
for worker in workers {
|
for worker in workers {
|
||||||
if worker.join().is_err() {
|
if worker.join().is_err() {
|
||||||
log::error!("the print worker stopped unexpectedly during shutdown");
|
log::error!("the print worker stopped unexpectedly during shutdown");
|
||||||
@@ -45,7 +45,7 @@ pub fn submit() -> Submit {
|
|||||||
let _ = sender.send(craft_print::submit(&bytes, &job).map_err(|error| error.to_string()));
|
let _ = sender.send(craft_print::submit(&bytes, &job).map_err(|error| error.to_string()));
|
||||||
});
|
});
|
||||||
if let Ok(worker) = worker {
|
if let Ok(worker) = worker {
|
||||||
let mut jobs = JOBS.lock().unwrap_or_else(|poison| poison.into_inner());
|
let mut jobs = JOBS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
jobs.retain(|job| !job.is_finished());
|
jobs.retain(|job| !job.is_finished());
|
||||||
jobs.push(worker);
|
jobs.push(worker);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ fn frame(ctx: &mut Ctx, area: Bx, cat: CatAxis, y: &ValAxis, y2: Option<&ValAxis
|
|||||||
let maxw = |ctx: &Ctx, l: &[(f64, String)]| l.iter().map(|(_, t)| ctx.tw(t, ls)).fold(0.0f32, f32::max);
|
let maxw = |ctx: &Ctx, l: &[(f64, String)]| l.iter().map(|(_, t)| ctx.tw(t, ls)).fold(0.0f32, f32::max);
|
||||||
if !horizontal {
|
if !horizontal {
|
||||||
let left = (maxw(ctx, &ylabels) + 6.0).min(area.w * 0.4);
|
let left = (maxw(ctx, &ylabels) + 6.0).min(area.w * 0.4);
|
||||||
let y2labels = y2.map(|a| a.labels());
|
let y2labels = y2.map(ValAxis::labels);
|
||||||
let right = match (&y2labels, &cat) {
|
let right = match (&y2labels, &cat) {
|
||||||
(Some(l), _) => (maxw(ctx, l) + 6.0).min(area.w * 0.3),
|
(Some(l), _) => (maxw(ctx, l) + 6.0).min(area.w * 0.3),
|
||||||
(None, CatAxis::Numeric(s, f)) => (ctx.tw(&f.format(s.max, Some(s.step)), ls) / 2.0).min(area.w * 0.2),
|
(None, CatAxis::Numeric(s, f)) => (ctx.tw(&f.format(s.max, Some(s.step)), ls) / 2.0).min(area.w * 0.2),
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ fn sample(kind: ChartKind) -> ChartData {
|
|||||||
ChartData {
|
ChartData {
|
||||||
category_levels: vec![],
|
category_levels: vec![],
|
||||||
palette: vec![],
|
palette: vec![],
|
||||||
categories: cats.iter().map(|s| s.to_string()).collect(),
|
categories: cats.iter().map(ToString::to_string).collect(),
|
||||||
series: vec![
|
series: vec![
|
||||||
series("North", &[12000.0, -4000.0, 23000.0, 31000.0, 18000.0, 27000.0], [20, 96, 130, 255], k1),
|
series("North", &[12000.0, -4000.0, 23000.0, 31000.0, 18000.0, 27000.0], [20, 96, 130, 255], k1),
|
||||||
b,
|
b,
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ fn is_r1c1_token(name: &str) -> bool {
|
|||||||
if chars.peek().is_some_and(|c| c.to_ascii_lowercase() == letter) {
|
if chars.peek().is_some_and(|c| c.to_ascii_lowercase() == letter) {
|
||||||
chars.next();
|
chars.next();
|
||||||
letters += 1;
|
letters += 1;
|
||||||
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
|
while chars.peek().is_some_and(char::is_ascii_digit) {
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ impl Value {
|
|||||||
Value::Bool(b) => Ok(if *b { "TRUE".into() } else { "FALSE".into() }),
|
Value::Bool(b) => Ok(if *b { "TRUE".into() } else { "FALSE".into() }),
|
||||||
Value::Text(t) => Ok(t.to_string()),
|
Value::Text(t) => Ok(t.to_string()),
|
||||||
Value::Error(e) => Err(*e),
|
Value::Error(e) => Err(*e),
|
||||||
Value::Array(a) => a.data.first().map(|v| v.to_text()).unwrap_or(Ok(String::new())),
|
Value::Array(a) => a.data.first().map(Value::to_text).unwrap_or(Ok(String::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Coercion to a boolean as `IF` does it.
|
/// Coercion to a boolean as `IF` does it.
|
||||||
@@ -264,7 +264,7 @@ impl Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::Error(e) => Err(*e),
|
Value::Error(e) => Err(*e),
|
||||||
Value::Array(a) => a.data.first().map(|v| v.to_bool()).unwrap_or(Ok(false)),
|
Value::Array(a) => a.data.first().map(Value::to_bool).unwrap_or(Ok(false)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Display text with General formatting (for logs, CSV and tests).
|
/// Display text with General formatting (for logs, CSV and tests).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use gridcraft_core::{CellRef, RangeRef, Value, compare};
|
use gridcraft_core::{CellRef, RangeRef, Value, compare};
|
||||||
use gridcraft_model::*;
|
use gridcraft_model::{CfOperator, CfRule, CfValueKind, Color, CondFormat, PatternType, Sheet, Style, Underline, Workbook, default_icon_thresholds};
|
||||||
|
|
||||||
/// The conditional look of one cell.
|
/// The conditional look of one cell.
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
|||||||
@@ -941,7 +941,7 @@ fn from_csv(s: &mut Session, p: &Json) -> Result<Json> {
|
|||||||
return Err(EngineError::Other("Nothing to import.".into()));
|
return Err(EngineError::Other("Nothing to import.".into()));
|
||||||
}
|
}
|
||||||
let name = str_param(p, "sheet").map(str::to_string).or_else(|| {
|
let name = str_param(p, "sheet").map(str::to_string).or_else(|| {
|
||||||
str_param(p, "path").and_then(|x| std::path::Path::new(x).file_stem().and_then(|f| f.to_str()).map(|f| f.chars().take(31).collect()))
|
str_param(p, "path").and_then(|x| std::path::Path::new(x).file_stem().and_then(std::ffi::OsStr::to_str).map(|f| f.chars().take(31).collect()))
|
||||||
});
|
});
|
||||||
let r = s.execute("home.insertSheet", json!({"name": name})).or_else(|_| s.execute("home.insertSheet", json!({})))?;
|
let r = s.execute("home.insertSheet", json!({"name": name})).or_else(|_| s.execute("home.insertSheet", json!({})))?;
|
||||||
let w = rows.iter().map(Vec::len).max().unwrap_or(1).max(1);
|
let w = rows.iter().map(Vec::len).max().unwrap_or(1).max(1);
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ fn open(s: &mut Session, p: &Json) -> Result<Json> {
|
|||||||
matches!(crate::io::FileKind::from_path(&name), Some(crate::io::FileKind::Ods | crate::io::FileKind::Xlsb | crate::io::FileKind::Xls))
|
matches!(crate::io::FileKind::from_path(&name), Some(crate::io::FileKind::Ods | crate::io::FileKind::Xlsb | crate::io::FileKind::Xls))
|
||||||
|| matches!(gridcraft_xlsx::sniff(&bytes), gridcraft_xlsx::Format::Ods | gridcraft_xlsx::Format::Xlsb | gridcraft_xlsx::Format::Xls);
|
|| matches!(gridcraft_xlsx::sniff(&bytes), gridcraft_xlsx::Format::Ods | gridcraft_xlsx::Format::Xlsb | gridcraft_xlsx::Format::Xls);
|
||||||
let title = if imported {
|
let title = if imported {
|
||||||
format!("{} (imported).xlsx", std::path::Path::new(&name).file_stem().and_then(|n| n.to_str()).unwrap_or("Book"))
|
format!("{} (imported).xlsx", std::path::Path::new(&name).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Book"))
|
||||||
} else {
|
} else {
|
||||||
std::path::Path::new(&name).file_name().and_then(|n| n.to_str()).unwrap_or("Book").to_string()
|
std::path::Path::new(&name).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("Book").to_string()
|
||||||
};
|
};
|
||||||
// An import has no save target: ordinary Save must ask for a new destination. Neither does a
|
// An import has no save target: ordinary Save must ask for a new destination. Neither does a
|
||||||
// workbook decrypted with its password, so Save never replaces the encrypted original with
|
// workbook decrypted with its password, so Save never replaces the encrypted original with
|
||||||
@@ -114,7 +114,7 @@ fn save(s: &mut Session, p: &Json) -> Result<Json> {
|
|||||||
};
|
};
|
||||||
// A PDF is a print of the sheet (Page Setup applies), not a copy of the workbook: the
|
// A PDF is a print of the sheet (Page Setup applies), not a copy of the workbook: the
|
||||||
// document keeps its file and its unsaved changes.
|
// document keeps its file and its unsaved changes.
|
||||||
if std::path::Path::new(&path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("pdf")) {
|
if std::path::Path::new(&path).extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("pdf")) {
|
||||||
return super::print::export_pdf(s, &json!({"path": path}));
|
return super::print::export_pdf(s, &json!({"path": path}));
|
||||||
}
|
}
|
||||||
let d = s.doc_mut()?;
|
let d = s.doc_mut()?;
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
|
|||||||
test_commands().iter().find(|c| c.id == id).copied()
|
test_commands().iter().find(|c| c.id == id).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
static TEST_COMMANDS: std::sync::Mutex<Vec<&'static CommandSpec>> = std::sync::Mutex::new(Vec::new());
|
static TEST_COMMANDS: std::sync::Mutex<Vec<&CommandSpec>> = std::sync::Mutex::new(Vec::new());
|
||||||
|
|
||||||
/// Test-only: register an extra command so UI integration tests can prove a pending
|
/// Test-only: register an extra command so UI integration tests can prove a pending
|
||||||
/// keytip becomes live the moment its command lands (see crates/ui-egui/tests/keytips.rs).
|
/// keytip becomes live the moment its command lands (see crates/ui-egui/tests/keytips.rs).
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ fn find_pivot(s: &Session, p: &Json) -> Result<(usize, usize)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn area_param(cmd: &str, v: Option<&str>) -> Result<&'static str> {
|
fn area_param(cmd: &str, v: Option<&str>) -> Result<&'static str> {
|
||||||
match v.map(|x| x.to_ascii_lowercase()) {
|
match v.map(str::to_ascii_lowercase) {
|
||||||
Some(a) if a == "rows" || a == "row" => Ok("rows"),
|
Some(a) if a == "rows" || a == "row" => Ok("rows"),
|
||||||
Some(a) if a == "columns" || a == "column" || a == "cols" => Ok("columns"),
|
Some(a) if a == "columns" || a == "column" || a == "cols" => Ok("columns"),
|
||||||
Some(a) if a == "values" || a == "value" || a == "data" => Ok("values"),
|
Some(a) if a == "values" || a == "value" || a == "data" => Ok("values"),
|
||||||
|
|||||||
@@ -617,7 +617,7 @@ pub fn expand_header_footer(spec: &str, cx: &HfContext) -> [HfSection; 3] {
|
|||||||
i += rest.chars().count() + 1;
|
i += rest.chars().count() + 1;
|
||||||
}
|
}
|
||||||
d if d.is_ascii_digit() => {
|
d if d.is_ascii_digit() => {
|
||||||
let digits: String = std::iter::once(d).chain(chars[i..].iter().copied().take_while(|c| c.is_ascii_digit())).take(3).collect();
|
let digits: String = std::iter::once(d).chain(chars[i..].iter().copied().take_while(char::is_ascii_digit)).take(3).collect();
|
||||||
i += digits.len() - 1;
|
i += digits.len() - 1;
|
||||||
if let Ok(v) = digits.parse::<f32>() {
|
if let Ok(v) = digits.parse::<f32>() {
|
||||||
sec.size = Some(v.clamp(1.0, 409.0));
|
sec.size = Some(v.clamp(1.0, 409.0));
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ use gridcraft_formula::adjust::{Axis, Edit};
|
|||||||
use gridcraft_model::{Anchor, AnchorMode, Sheet, Visibility};
|
use gridcraft_model::{Anchor, AnchorMode, Sheet, Visibility};
|
||||||
use serde_json::{Value as Json, json};
|
use serde_json::{Value as Json, json};
|
||||||
|
|
||||||
use super::*;
|
use super::{
|
||||||
|
CommandSpec, Ctx, EngineError, PROTECTED, Result, Session, bad, bool_param, cell_param_on, cmd, edit, has_clipboard, has_doc, ok, parse_lines,
|
||||||
|
str_param, target_range, target_sheet, u32_param,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn specs() -> Vec<CommandSpec> {
|
pub fn specs() -> Vec<CommandSpec> {
|
||||||
vec![
|
vec![
|
||||||
@@ -270,6 +273,29 @@ fn cancel_copy_mode(s: &mut Session) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inherit_line_format(sh: &mut Sheet, axis: Axis, at: u32, count: u32) {
|
||||||
|
if at == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let prev = if axis == Axis::Rows { sh.rows.get(&(at - 1)).copied() } else { sh.cols.get(&(at - 1)).copied() };
|
||||||
|
if let Some(info) = prev {
|
||||||
|
for i in at..at + count {
|
||||||
|
let map = if axis == Axis::Rows { &mut sh.rows } else { &mut sh.cols };
|
||||||
|
map.insert(i, gridcraft_model::LineInfo { hidden: false, outline: 0, collapsed: false, ..info });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_push_off_sheet(s: &Session, sheet: usize, axis: Axis, at: u32, count: u32, limit: u32) -> Result<()> {
|
||||||
|
if let Some(u) = s.doc()?.wb.sheet(sheet).and_then(|sh| sh.used_range()) {
|
||||||
|
let last = if axis == Axis::Rows { u.end.row } else { u.end.col };
|
||||||
|
if last >= at && last as u64 + count as u64 >= limit as u64 {
|
||||||
|
return Err(EngineError::Other("To prevent possible loss of data, Excel cannot shift nonblank cells off of the worksheet.".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
|
fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
|
||||||
let (at, count) = lines(s, p, axis)?;
|
let (at, count) = lines(s, p, axis)?;
|
||||||
let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS };
|
let limit = if axis == Axis::Rows { MAX_ROWS } else { MAX_COLS };
|
||||||
@@ -279,13 +305,7 @@ fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
|
|||||||
}
|
}
|
||||||
check_lines_protection(s, p, axis, at, count, true)?;
|
check_lines_protection(s, p, axis, at, count, true)?;
|
||||||
let sheet = target_sheet(s, p)?;
|
let sheet = target_sheet(s, p)?;
|
||||||
// Refuse to push data off the sheet.
|
check_push_off_sheet(s, sheet, axis, at, count, limit)?;
|
||||||
if let Some(u) = s.doc()?.wb.sheet(sheet).and_then(|sh| sh.used_range()) {
|
|
||||||
let last = if axis == Axis::Rows { u.end.row } else { u.end.col };
|
|
||||||
if last >= at && last as u64 + count as u64 >= limit as u64 {
|
|
||||||
return Err(EngineError::Other("To prevent possible loss of data, Excel cannot shift nonblank cells off of the worksheet.".into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let out = edit(s, |cx| {
|
let out = edit(s, |cx| {
|
||||||
let name = cx.wb.sheet(sheet).map(|s| s.name.clone()).unwrap_or_default();
|
let name = cx.wb.sheet(sheet).map(|s| s.name.clone()).unwrap_or_default();
|
||||||
cx.protection_checked = true;
|
cx.protection_checked = true;
|
||||||
@@ -297,15 +317,7 @@ fn insert_lines(s: &mut Session, p: &Json, axis: Axis) -> Result<Json> {
|
|||||||
}
|
}
|
||||||
shift_sheet_features(sh, axis, at, count, true);
|
shift_sheet_features(sh, axis, at, count, true);
|
||||||
// New lines take the format of the line before (Excel's "Format Same As Above").
|
// New lines take the format of the line before (Excel's "Format Same As Above").
|
||||||
if at > 0 {
|
inherit_line_format(sh, axis, at, count);
|
||||||
let prev = if axis == Axis::Rows { sh.rows.get(&(at - 1)).copied() } else { sh.cols.get(&(at - 1)).copied() };
|
|
||||||
if let Some(info) = prev {
|
|
||||||
for i in at..at + count {
|
|
||||||
let map = if axis == Axis::Rows { &mut sh.rows } else { &mut sh.cols };
|
|
||||||
map.insert(i, gridcraft_model::LineInfo { hidden: false, outline: 0, collapsed: false, ..info });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
rewrite(&mut cx.wb, &name, &Edit::Insert { axis, at, count });
|
rewrite(&mut cx.wb, &name, &Edit::Insert { axis, at, count });
|
||||||
cx.structural = true;
|
cx.structural = true;
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ use gridcraft_core::{CellRef, RangeRef, Value};
|
|||||||
use gridcraft_model::{Cell, DefinedName, Sheet, Workbook};
|
use gridcraft_model::{Cell, DefinedName, Sheet, Workbook};
|
||||||
use serde_json::{Value as Json, json};
|
use serde_json::{Value as Json, json};
|
||||||
|
|
||||||
|
use super::check_arrays;
|
||||||
use super::whatif::Trial;
|
use super::whatif::Trial;
|
||||||
use super::*;
|
use super::{CommandSpec, Ctx, DocState, EngineError, Result, Session, bad, bool_param, cmd, edit, f64_param, has_doc, str_param};
|
||||||
|
use gridcraft_core::split_sheet;
|
||||||
|
|
||||||
const CMD: &str = "data.solver";
|
const CMD: &str = "data.solver";
|
||||||
const REPORT_CMD: &str = "data.solverReport";
|
const REPORT_CMD: &str = "data.solverReport";
|
||||||
@@ -1705,6 +1707,26 @@ fn title(out: &mut Lines, kind: ReportKind, sheet: &str) -> u32 {
|
|||||||
3
|
3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_constraint_row(sh: &Sheet, c: &Constraint, cell: CellRef, v: f64, rhs: &[f64], k: usize) -> Vec<Value> {
|
||||||
|
let (formula, status, slack) = match (c.rel, &c.rhs) {
|
||||||
|
(Rel::Int, _) => (format!("{}=Integer", abs_ref(cell)), "Binding", 0.0),
|
||||||
|
(Rel::Bin, _) => (format!("{}=Binary", abs_ref(cell)), "Binding", 0.0),
|
||||||
|
(rel, rhs_cells) => {
|
||||||
|
let b = rhs.get(k).or_else(|| rhs.first()).copied().unwrap_or(f64::NAN);
|
||||||
|
let rhs_text = match rhs_cells {
|
||||||
|
Rhs::Cells(cells) => cells.get(k).or_else(|| cells.first()).map(|c| abs_ref(*c)).unwrap_or_default(),
|
||||||
|
_ => gridcraft_core::number_to_text(b),
|
||||||
|
};
|
||||||
|
let slack = (v - b).abs();
|
||||||
|
let binding = rel == Rel::Eq || slack <= PRECISION * (1.0 + b.abs());
|
||||||
|
let slack = if binding { 0.0 } else { slack };
|
||||||
|
(format!("{}{}{rhs_text}", abs_ref(cell), rel.text()), if binding { "Binding" } else { "Not Binding" }, slack)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let num = |v: f64| if v.is_finite() { Value::number(v) } else { Value::Error(gridcraft_core::CellError::Value) };
|
||||||
|
vec![Value::text(abs_ref(cell)), Value::text(cell_name(sh, cell)), num(v), Value::text(formula), Value::text(status), num(slack)]
|
||||||
|
}
|
||||||
|
|
||||||
fn answer_report(d: &DocState, sh: &Sheet, inp: &ReportInput) -> Lines {
|
fn answer_report(d: &DocState, sh: &Sheet, inp: &ReportInput) -> Lines {
|
||||||
let m = inp.model;
|
let m = inp.model;
|
||||||
let mut trial = Trial::new(d);
|
let mut trial = Trial::new(d);
|
||||||
@@ -1768,29 +1790,7 @@ fn answer_report(d: &DocState, sh: &Sheet, inp: &ReportInput) -> Lines {
|
|||||||
Rhs::None => vec![],
|
Rhs::None => vec![],
|
||||||
};
|
};
|
||||||
for (k, (cell, v)) in c.lhs.iter().zip(&lhs).enumerate() {
|
for (k, (cell, v)) in c.lhs.iter().zip(&lhs).enumerate() {
|
||||||
let (formula, status, slack) = match (c.rel, &c.rhs) {
|
rows.push(format_constraint_row(sh, c, *cell, *v, &rhs, k));
|
||||||
(Rel::Int, _) => (format!("{}=Integer", abs_ref(*cell)), "Binding", 0.0),
|
|
||||||
(Rel::Bin, _) => (format!("{}=Binary", abs_ref(*cell)), "Binding", 0.0),
|
|
||||||
(rel, rhs_cells) => {
|
|
||||||
let b = rhs.get(k).or_else(|| rhs.first()).copied().unwrap_or(f64::NAN);
|
|
||||||
let rhs_text = match rhs_cells {
|
|
||||||
Rhs::Cells(cells) => cells.get(k).or_else(|| cells.first()).map(|c| abs_ref(*c)).unwrap_or_default(),
|
|
||||||
_ => gridcraft_core::number_to_text(b),
|
|
||||||
};
|
|
||||||
let slack = (v - b).abs();
|
|
||||||
let binding = rel == Rel::Eq || slack <= PRECISION * (1.0 + b.abs());
|
|
||||||
let slack = if binding { 0.0 } else { slack };
|
|
||||||
(format!("{}{}{rhs_text}", abs_ref(*cell), rel.text()), if binding { "Binding" } else { "Not Binding" }, slack)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
rows.push(vec![
|
|
||||||
Value::text(abs_ref(*cell)),
|
|
||||||
Value::text(cell_name(sh, *cell)),
|
|
||||||
num(*v),
|
|
||||||
Value::text(formula),
|
|
||||||
Value::text(status),
|
|
||||||
num(slack),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
table(&mut out, r + 1, &["Cell", "Name", "Cell Value", "Formula", "Status", "Slack"], rows);
|
table(&mut out, r + 1, &["Cell", "Name", "Cell Value", "Formula", "Status", "Slack"], rows);
|
||||||
|
|||||||
@@ -527,9 +527,9 @@ fn ordinal_suffix(n: i64, suffix: &str) -> String {
|
|||||||
/// `Item 007` → ("Item ", 7, "", 3); `3rd` style leading numbers → ("", 3, "rd", 1).
|
/// `Item 007` → ("Item ", 7, "", 3); `3rd` style leading numbers → ("", 3, "rd", 1).
|
||||||
fn split_num(t: &str) -> Option<(String, i64, String, usize)> {
|
fn split_num(t: &str) -> Option<(String, i64, String, usize)> {
|
||||||
let chars: Vec<char> = t.chars().collect();
|
let chars: Vec<char> = t.chars().collect();
|
||||||
let end = chars.iter().rposition(|c| c.is_ascii_digit())?;
|
let end = chars.iter().rposition(char::is_ascii_digit)?;
|
||||||
let mut start = end;
|
let mut start = end;
|
||||||
while start > 0 && chars.get(start - 1).is_some_and(|c| c.is_ascii_digit()) {
|
while start > 0 && chars.get(start - 1).is_some_and(char::is_ascii_digit) {
|
||||||
start -= 1;
|
start -= 1;
|
||||||
}
|
}
|
||||||
let digits: String = chars.get(start..=end)?.iter().collect();
|
let digits: String = chars.get(start..=end)?.iter().collect();
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ fn part_too_large_message(name: &str, part: &str, size: u64) -> String {
|
|||||||
|
|
||||||
/// A CSV opens as a sheet named after the file (like Excel).
|
/// A CSV opens as a sheet named after the file (like Excel).
|
||||||
fn rename_first_sheet(wb: &mut Workbook, path: &str) {
|
fn rename_first_sheet(wb: &mut Workbook, path: &str) {
|
||||||
let stem = std::path::Path::new(path).file_stem().and_then(|s| s.to_str()).unwrap_or("Sheet1");
|
let stem = std::path::Path::new(path).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Sheet1");
|
||||||
let clean: String = stem.chars().filter(|c| !matches!(c, ':' | '\\' | '/' | '?' | '*' | '[' | ']')).take(31).collect();
|
let clean: String = stem.chars().filter(|c| !matches!(c, ':' | '\\' | '/' | '?' | '*' | '[' | ']')).take(31).collect();
|
||||||
let clean = clean.trim_matches('\'').to_string();
|
let clean = clean.trim_matches('\'').to_string();
|
||||||
// "History" is reserved (see `Workbook::check_sheet_name`): keep the default name instead.
|
// "History" is reserved (see `Workbook::check_sheet_name`): keep the default name instead.
|
||||||
@@ -226,7 +226,7 @@ pub fn save_bytes(wb: &Workbook, path: &str) -> Result<Vec<u8>> {
|
|||||||
// An unknown or missing extension is refused rather than written as an XLSX under that name:
|
// An unknown or missing extension is refused rather than written as an XLSX under that name:
|
||||||
// a typo like `.cvs` would otherwise produce a file no other app recognises.
|
// a typo like `.cvs` would otherwise produce a file no other app recognises.
|
||||||
let Some(kind) = FileKind::from_path(path) else {
|
let Some(kind) = FileKind::from_path(path) else {
|
||||||
let name = std::path::Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(path);
|
let name = std::path::Path::new(path).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(path);
|
||||||
return Err(EngineError::Other(format!("{name}: unknown file format. Save as .xlsx, .csv, .tsv, .json or .html.")));
|
return Err(EngineError::Other(format!("{name}: unknown file format. Save as .xlsx, .csv, .tsv, .json or .html.")));
|
||||||
};
|
};
|
||||||
save_bytes_as(wb, path, kind)
|
save_bytes_as(wb, path, kind)
|
||||||
@@ -239,7 +239,7 @@ pub fn save_bytes_as(wb: &Workbook, path: &str, kind: FileKind) -> Result<Vec<u8
|
|||||||
// The extension picks the package kind; only the macro-enabled kinds (.xlsm, .xltm) carry
|
// The extension picks the package kind; only the macro-enabled kinds (.xlsm, .xltm) carry
|
||||||
// the VBA project, so any other name saves without it, as Excel does.
|
// the VBA project, so any other name saves without it, as Excel does.
|
||||||
FileKind::Xlsx => {
|
FileKind::Xlsx => {
|
||||||
let book = std::path::Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or_default();
|
let book = std::path::Path::new(path).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or_default();
|
||||||
gridcraft_xlsx::write_xlsx_named(wb, gridcraft_xlsx::XlsxKind::from_path(path), book).map_err(|e| EngineError::Other(e.to_string()))
|
gridcraft_xlsx::write_xlsx_named(wb, gridcraft_xlsx::XlsxKind::from_path(path), book).map_err(|e| EngineError::Other(e.to_string()))
|
||||||
}
|
}
|
||||||
FileKind::Xls => {
|
FileKind::Xls => {
|
||||||
|
|||||||
@@ -601,8 +601,11 @@ impl Session {
|
|||||||
let result = match result {
|
let result = match result {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(p) => {
|
Err(p) => {
|
||||||
let msg =
|
let msg = p
|
||||||
p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
|
.downcast_ref::<&str>()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.or_else(|| p.downcast_ref::<String>().cloned())
|
||||||
|
.unwrap_or_else(|| "panic".into());
|
||||||
// Restore the workbook and its history as they were, and drop the undo groups the
|
// Restore the workbook and its history as they were, and drop the undo groups the
|
||||||
// command opened.
|
// command opened.
|
||||||
self.groups.retain(|g| g.id < first_group);
|
self.groups.retain(|g| g.id < first_group);
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use gridcraft_core::date::datetime_from_serial;
|
use gridcraft_core::date::datetime_from_serial;
|
||||||
pub use gridcraft_core::quote_sheet;
|
pub use gridcraft_core::quote_sheet;
|
||||||
use gridcraft_core::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value, split_sheet};
|
use gridcraft_core::{CellError, CellRef, MAX_COLS, MAX_ROWS, RangeRef, Value, split_sheet};
|
||||||
use gridcraft_model::*;
|
use gridcraft_model::{
|
||||||
|
BorderLine, BorderStyle, Cell, Chart, Color, Fill, HAlign, NumFmt, PivotDateGroup, PivotField, PivotFilter, PivotFunc, PivotLayout, PivotShowAs,
|
||||||
|
PivotSort, PivotTable, PivotValue, Series, Style, StyleId, Workbook,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::pivot_calc::CalcSet;
|
use crate::pivot_calc::CalcSet;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
//! `TableStyleLight1…21 / Medium1…28 / Dark1…11` names so files round-trip).
|
//! `TableStyleLight1…21 / Medium1…28 / Dark1…11` names so files round-trip).
|
||||||
|
|
||||||
use gridcraft_core::{CellRef, RangeRef};
|
use gridcraft_core::{CellRef, RangeRef};
|
||||||
use gridcraft_model::*;
|
use gridcraft_model::{BorderLine, BorderStyle, Cell, Color, Fill, Formula, PatternType, Style, Table, Workbook};
|
||||||
|
|
||||||
/// (family, number) from a style name, e.g. `TableStyleMedium2` → ("Medium", 2).
|
/// (family, number) from a style name, e.g. `TableStyleMedium2` → ("Medium", 2).
|
||||||
pub fn parse_style_name(name: &str) -> (&'static str, u32) {
|
pub fn parse_style_name(name: &str) -> (&'static str, u32) {
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ fn guarded<T>(step: usize, kind: &'static str, f: impl FnOnce() -> Result<T, Str
|
|||||||
Ok(Ok(v)) => Ok(v),
|
Ok(Ok(v)) => Ok(v),
|
||||||
Ok(Err(e)) => Err(fail(step, kind, e)),
|
Ok(Err(e)) => Err(fail(step, kind, e)),
|
||||||
Err(p) => {
|
Err(p) => {
|
||||||
let msg = p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
let msg = p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
|
||||||
Err(fail(step, kind, format!("panicked: {msg}")))
|
Err(fail(step, kind, format!("panicked: {msg}")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use gridcraft_core::{CellError, MAX_COLS, MAX_ROWS, RangeRef};
|
use gridcraft_core::{CellError, MAX_COLS, MAX_ROWS, RangeRef};
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::{Anchor, Expr, RefKind, Reference, SheetSel, clamp_anchor};
|
||||||
|
|
||||||
/// Copy/fill: relative parts move by `(dr, dc)`. References pushed off the sheet become `#REF!`.
|
/// Copy/fill: relative parts move by `(dr, dc)`. References pushed off the sheet become `#REF!`.
|
||||||
pub fn shift_relative(e: Expr, dr: i64, dc: i64) -> Expr {
|
pub fn shift_relative(e: Expr, dr: i64, dc: i64) -> Expr {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
|||||||
use gridcraft_core::addr::{letters_to_col, parse_a1_prefix};
|
use gridcraft_core::addr::{letters_to_col, parse_a1_prefix};
|
||||||
use gridcraft_core::{CellError, MAX_ROWS};
|
use gridcraft_core::{CellError, MAX_ROWS};
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::{Anchor, BinOp, Expr, RefKind, Reference, SheetSel, StructItem, StructRef, TrimMode, UnOp};
|
||||||
use crate::lexer::{Tok, Token, tokenize_locale};
|
use crate::lexer::{Tok, Token, tokenize_locale};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::fmt::Write;
|
|||||||
|
|
||||||
use gridcraft_core::{CellRef, col_to_letters, number_to_text};
|
use gridcraft_core::{CellRef, col_to_letters, number_to_text};
|
||||||
|
|
||||||
use crate::ast::*;
|
use crate::ast::{Anchor, BinOp, Expr, RefKind, Reference, SheetSel, StructItem, StructRef, UnOp};
|
||||||
|
|
||||||
pub use gridcraft_core::{quote_sheet, sheet_needs_quotes as needs_quotes};
|
pub use gridcraft_core::{quote_sheet, sheet_needs_quotes as needs_quotes};
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,60 @@ impl Criterion {
|
|||||||
Criterion { op, operand, bare_empty: false }
|
Criterion { op, operand, bare_empty: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn matches_blank(&self, v: &Value) -> bool {
|
||||||
|
let blank = matches!(v, Value::Empty);
|
||||||
|
match self.op {
|
||||||
|
Op::Eq => blank,
|
||||||
|
Op::Ne => !blank,
|
||||||
|
// "<" / ">" with nothing: Excel compares against "" for text cells, but an
|
||||||
|
// empty string never matches ("<=" and ">=" count it in neither).
|
||||||
|
_ => match v {
|
||||||
|
Value::Text(t) if !t.is_empty() => cmp_op(self.op, compare_text(t, "")),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_number(&self, v: &Value, n: f64) -> bool {
|
||||||
|
let cell = match v {
|
||||||
|
Value::Number(x) => Some(*x),
|
||||||
|
// Numeric text matches only for equality (COUNTIF("1") counts text "1").
|
||||||
|
Value::Text(t) if matches!(self.op, Op::Eq | Op::Ne) => parse_number_text(t),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
match cell {
|
||||||
|
Some(x) => cmp_op(self.op, compare_numbers(x, n)),
|
||||||
|
None => self.op == Op::Ne,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_text(&self, v: &Value, pat: &str) -> bool {
|
||||||
|
match self.op {
|
||||||
|
Op::Eq | Op::Ne => {
|
||||||
|
// Blank cells hold no text: `"*"` skips them and `"<>*"` counts them.
|
||||||
|
let cell_text = match v {
|
||||||
|
Value::Text(t) => Some(t.to_string()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let m = cell_text.is_some_and(|t| wildcard_match(pat, &t));
|
||||||
|
if self.op == Op::Eq { m } else { !m }
|
||||||
|
}
|
||||||
|
_ => match v {
|
||||||
|
Value::Text(t) => cmp_op(self.op, compare_text(t, pat)),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_cut_text(&self, v: &Value, kept: &str) -> bool {
|
||||||
|
match self.op {
|
||||||
|
Op::Eq => false,
|
||||||
|
Op::Ne => true,
|
||||||
|
Op::Lt | Op::Le => matches!(v, Value::Text(t) if compare_text(t, kept) != Ordering::Greater),
|
||||||
|
Op::Gt | Op::Ge => matches!(v, Value::Text(t) if compare_text(t, kept) == Ordering::Greater),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether a cell value satisfies the criterion.
|
/// Whether a cell value satisfies the criterion.
|
||||||
pub(crate) fn matches(&self, v: &Value) -> bool {
|
pub(crate) fn matches(&self, v: &Value) -> bool {
|
||||||
let v = match v {
|
let v = match v {
|
||||||
@@ -133,31 +187,8 @@ impl Criterion {
|
|||||||
return matches!(&v, Value::Empty) || matches!(&v, Value::Text(t) if t.is_empty());
|
return matches!(&v, Value::Empty) || matches!(&v, Value::Text(t) if t.is_empty());
|
||||||
}
|
}
|
||||||
match &self.operand {
|
match &self.operand {
|
||||||
Operand::Blank => {
|
Operand::Blank => self.matches_blank(&v),
|
||||||
let blank = matches!(v, Value::Empty);
|
Operand::Number(n) => self.matches_number(&v, *n),
|
||||||
match self.op {
|
|
||||||
Op::Eq => blank,
|
|
||||||
Op::Ne => !blank,
|
|
||||||
// "<" / ">" with nothing: Excel compares against "" for text cells, but an
|
|
||||||
// empty string never matches ("<=" and ">=" count it in neither).
|
|
||||||
_ => match &v {
|
|
||||||
Value::Text(t) if !t.is_empty() => cmp_op(self.op, compare_text(t, "")),
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Operand::Number(n) => {
|
|
||||||
let cell = match &v {
|
|
||||||
Value::Number(x) => Some(*x),
|
|
||||||
// Numeric text matches only for equality (COUNTIF("1") counts text "1").
|
|
||||||
Value::Text(t) if matches!(self.op, Op::Eq | Op::Ne) => parse_number_text(t),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
match cell {
|
|
||||||
Some(x) => cmp_op(self.op, compare_numbers(x, *n)),
|
|
||||||
None => self.op == Op::Ne,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Operand::Bool(b) => match &v {
|
Operand::Bool(b) => match &v {
|
||||||
Value::Bool(x) => cmp_op(self.op, x.cmp(b)),
|
Value::Bool(x) => cmp_op(self.op, x.cmp(b)),
|
||||||
_ => self.op == Op::Ne,
|
_ => self.op == Op::Ne,
|
||||||
@@ -166,27 +197,8 @@ impl Criterion {
|
|||||||
Value::Error(x) => cmp_op(self.op, x.code().cmp(&e.code())),
|
Value::Error(x) => cmp_op(self.op, x.code().cmp(&e.code())),
|
||||||
_ => self.op == Op::Ne,
|
_ => self.op == Op::Ne,
|
||||||
},
|
},
|
||||||
Operand::Text(pat) => match self.op {
|
Operand::Text(pat) => self.matches_text(&v, pat),
|
||||||
Op::Eq | Op::Ne => {
|
Operand::CutText(kept) => self.matches_cut_text(&v, kept),
|
||||||
// Blank cells hold no text: `"*"` skips them and `"<>*"` counts them.
|
|
||||||
let cell_text = match &v {
|
|
||||||
Value::Text(t) => Some(t.to_string()),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let m = cell_text.is_some_and(|t| wildcard_match(pat, &t));
|
|
||||||
if self.op == Op::Eq { m } else { !m }
|
|
||||||
}
|
|
||||||
_ => match &v {
|
|
||||||
Value::Text(t) => cmp_op(self.op, compare_text(t, pat)),
|
|
||||||
_ => false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Operand::CutText(kept) => match self.op {
|
|
||||||
Op::Eq => false,
|
|
||||||
Op::Ne => true,
|
|
||||||
Op::Lt | Op::Le => matches!(&v, Value::Text(t) if compare_text(t, kept) != Ordering::Greater),
|
|
||||||
Op::Gt | Op::Ge => matches!(&v, Value::Text(t) if compare_text(t, kept) == Ordering::Greater),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -742,7 +742,7 @@ fn im_binary(a: &[Arg], f: fn(Cx, Cx) -> R<Cx>) -> R<Value> {
|
|||||||
fmt_complex(f(x, y)?, s.unwrap_or('i'))
|
fmt_complex(f(x, y)?, s.unwrap_or('i'))
|
||||||
}
|
}
|
||||||
fn imdiv(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn imdiv(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
im_binary(a, |x, y| x.div(y))
|
im_binary(a, Cx::div)
|
||||||
}
|
}
|
||||||
fn imsub(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
fn imsub(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||||
im_binary(a, |x, y| Ok(x.sub(y)))
|
im_binary(a, |x, y| Ok(x.sub(y)))
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ fn aggregate(kind: Aggregate, xs: &mut [f64]) -> Option<f64> {
|
|||||||
Aggregate::Max => xs.iter().copied().reduce(f64::max),
|
Aggregate::Max => xs.iter().copied().reduce(f64::max),
|
||||||
Aggregate::Min => xs.iter().copied().reduce(f64::min),
|
Aggregate::Min => xs.iter().copied().reduce(f64::min),
|
||||||
Aggregate::Median => {
|
Aggregate::Median => {
|
||||||
xs.sort_by(|a, b| a.total_cmp(b));
|
xs.sort_by(f64::total_cmp);
|
||||||
let k = xs.len() / 2;
|
let k = xs.len() / 2;
|
||||||
let hi = xs.get(k).copied()?;
|
let hi = xs.get(k).copied()?;
|
||||||
if xs.len() % 2 == 1 { Some(hi) } else { Some((xs.get(k.checked_sub(1)?).copied()? + hi) / 2.0) }
|
if xs.len() % 2 == 1 { Some(hi) } else { Some((xs.get(k.checked_sub(1)?).copied()? + hi) / 2.0) }
|
||||||
@@ -415,7 +415,7 @@ impl Timeline {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut sorted = times.to_vec();
|
let mut sorted = times.to_vec();
|
||||||
sorted.sort_by(|a, b| a.total_cmp(b));
|
sorted.sort_by(f64::total_cmp);
|
||||||
sorted.dedup();
|
sorted.dedup();
|
||||||
let axis = detect_axis(&sorted, sys).ok()?;
|
let axis = detect_axis(&sorted, sys).ok()?;
|
||||||
let last = axis.position(*sorted.last()?)?.round();
|
let last = axis.position(*sorted.last()?)?.round();
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ impl FnSpec {
|
|||||||
pub(crate) const VAR: usize = usize::MAX;
|
pub(crate) const VAR: usize = usize::MAX;
|
||||||
|
|
||||||
fn registry() -> &'static (Vec<FnSpec>, HashMap<&'static str, usize>) {
|
fn registry() -> &'static (Vec<FnSpec>, HashMap<&'static str, usize>) {
|
||||||
static REG: OnceLock<(Vec<FnSpec>, HashMap<&'static str, usize>)> = OnceLock::new();
|
static REG: OnceLock<(Vec<FnSpec>, HashMap<&str, usize>)> = OnceLock::new();
|
||||||
REG.get_or_init(|| {
|
REG.get_or_init(|| {
|
||||||
let mut v: Vec<FnSpec> = Vec::new();
|
let mut v: Vec<FnSpec> = Vec::new();
|
||||||
for list in [
|
for list in [
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ fn var_p(x: &[f64]) -> R<f64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sorted(mut v: Vec<f64>) -> Vec<f64> {
|
fn sorted(mut v: Vec<f64>) -> Vec<f64> {
|
||||||
v.sort_by(|a, b| a.total_cmp(b));
|
v.sort_by(f64::total_cmp);
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ fn find_matches(text: &[char], delims: &[Vec<char>], insensitive: bool, from_end
|
|||||||
let mut j = t.len();
|
let mut j = t.len();
|
||||||
while j > 0 {
|
while j > 0 {
|
||||||
let best =
|
let best =
|
||||||
ds.iter().filter(|d| j.checked_sub(d.len()).and_then(|st| t.get(st..j)).is_some_and(|w| w == d.as_slice())).map(|d| d.len()).max();
|
ds.iter().filter(|d| j.checked_sub(d.len()).and_then(|st| t.get(st..j)).is_some_and(|w| w == d.as_slice())).map(Vec::len).max();
|
||||||
match best {
|
match best {
|
||||||
Some(l) => {
|
Some(l) => {
|
||||||
out.push((j - l, j));
|
out.push((j - l, j));
|
||||||
@@ -537,7 +537,7 @@ fn find_matches(text: &[char], delims: &[Vec<char>], insensitive: bool, from_end
|
|||||||
}
|
}
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < t.len() {
|
while i < t.len() {
|
||||||
let best = ds.iter().filter(|d| t.get(i..i + d.len()).is_some_and(|w| w == d.as_slice())).map(|d| d.len()).max();
|
let best = ds.iter().filter(|d| t.get(i..i + d.len()).is_some_and(|w| w == d.as_slice())).map(Vec::len).max();
|
||||||
match best {
|
match best {
|
||||||
Some(l) => {
|
Some(l) => {
|
||||||
out.push((i, i + l));
|
out.push((i, i + l));
|
||||||
@@ -588,7 +588,7 @@ fn textafter(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn split_by(s: &[char], delims: &[Vec<char>], insensitive: bool) -> Vec<Vec<char>> {
|
fn split_by(s: &[char], delims: &[Vec<char>], insensitive: bool) -> Vec<Vec<char>> {
|
||||||
if delims.iter().all(|d| d.is_empty()) {
|
if delims.iter().all(Vec::is_empty) {
|
||||||
return vec![s.to_vec()];
|
return vec![s.to_vec()];
|
||||||
}
|
}
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
@@ -748,9 +748,9 @@ fn convert_replacement(r: &str) -> String {
|
|||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < c.len() {
|
while i < c.len() {
|
||||||
let ch = c[i];
|
let ch = c[i];
|
||||||
if ch == '$' && c.get(i + 1).is_some_and(|d| d.is_ascii_digit()) {
|
if ch == '$' && c.get(i + 1).is_some_and(char::is_ascii_digit) {
|
||||||
let mut j = i + 1;
|
let mut j = i + 1;
|
||||||
while c.get(j).is_some_and(|d| d.is_ascii_digit()) {
|
while c.get(j).is_some_and(char::is_ascii_digit) {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
out.push_str("${");
|
out.push_str("${");
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ use gridcraft_core::{CellRef, RangeRef, Value};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::cell::{Cell, CellPicture};
|
use crate::cell::{Cell, CellPicture};
|
||||||
use crate::features::*;
|
use crate::features::{
|
||||||
|
AutoFilter, Chart, Comment, CondFormat, Hyperlink, Image, LineInfo, PivotTable, PrintSettings, Shape, SheetProtection, Slicer, Sparkline, Table,
|
||||||
|
Validation,
|
||||||
|
};
|
||||||
use crate::store::CellStore;
|
use crate::store::CellStore;
|
||||||
use crate::style::{Color, StyleId};
|
use crate::style::{Color, StyleId};
|
||||||
|
|
||||||
@@ -622,8 +625,11 @@ mod tests {
|
|||||||
|
|
||||||
// CellRef is a struct, so encode the sparse map as entries for JSON as well as other formats.
|
// CellRef is a struct, so encode the sparse map as entries for JSON as well as other formats.
|
||||||
mod picture_map {
|
mod picture_map {
|
||||||
use super::*;
|
use crate::cell::CellPicture;
|
||||||
use serde::{Deserializer, Serializer};
|
use gridcraft_core::CellRef;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
pub fn serialize<S: Serializer>(map: &BTreeMap<CellRef, Arc<CellPicture>>, serializer: S) -> Result<S::Ok, S::Error> {
|
pub fn serialize<S: Serializer>(map: &BTreeMap<CellRef, Arc<CellPicture>>, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
map.iter().collect::<Vec<_>>().serialize(serializer)
|
map.iter().collect::<Vec<_>>().serialize(serializer)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -525,7 +525,7 @@ fn render_fraction(sec: &Section, x: f64, pieces: &mut [String], lits: &mut [Opt
|
|||||||
let s = if k + 1 == count {
|
let s = if k + 1 == count {
|
||||||
dc.get(k..).map(|r| r.iter().collect::<String>()).unwrap_or_default()
|
dc.get(k..).map(|r| r.iter().collect::<String>()).unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
dc.get(k).map(|c| c.to_string()).unwrap_or_default()
|
dc.get(k).map(ToString::to_string).unwrap_or_default()
|
||||||
};
|
};
|
||||||
let s = if s.is_empty() {
|
let s = if s.is_empty() {
|
||||||
match toks.get(ti) {
|
match toks.get(ti) {
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ pub(crate) fn fold(c: char) -> Option<char> {
|
|||||||
'ž' => 'z',
|
'ž' => 'z',
|
||||||
'Œ' => 'W',
|
'Œ' => 'W',
|
||||||
'œ' => 'm',
|
'œ' => 'm',
|
||||||
'‘' | '’' | '‚' => '\'',
|
'‘'..='‚' => '\'',
|
||||||
'“' | '”' | '„' => '"',
|
'“'..='„' => '"',
|
||||||
'–' | '€' | '£' | '¥' | '¢' | '¤' => '0',
|
'–' | '€' | '£' | '¥' | '¢' | '¤' => '0',
|
||||||
'\u{a0}' => ' ',
|
'\u{a0}' => ' ',
|
||||||
'·' | '•' => '.',
|
'·' | '•' => '.',
|
||||||
|
|||||||
@@ -352,8 +352,8 @@ fn new_tiles(app: &mut SheetApp, ui: &mut Ui, out: &mut Outcome) {
|
|||||||
/// File name and folder of a recent path, split as text.
|
/// File name and folder of a recent path, split as text.
|
||||||
fn split_path(path: &str) -> (String, String) {
|
fn split_path(path: &str) -> (String, String) {
|
||||||
let p = std::path::Path::new(path);
|
let p = std::path::Path::new(path);
|
||||||
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or(path).to_string();
|
let name = p.file_name().and_then(std::ffi::OsStr::to_str).unwrap_or(path).to_string();
|
||||||
let folder = p.parent().and_then(|f| f.to_str()).unwrap_or("").to_string();
|
let folder = p.parent().and_then(std::path::Path::to_str).unwrap_or("").to_string();
|
||||||
(name, folder)
|
(name, folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,7 +888,7 @@ pub fn export_as(app: &mut SheetApp, fmt: &str) -> bool {
|
|||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
let title = app.session.active().map(|d| d.display_title()).unwrap_or_else(|| "Book1".into());
|
let title = app.session.active().map(|d| d.display_title()).unwrap_or_else(|| "Book1".into());
|
||||||
let stem = std::path::Path::new(&title).file_stem().and_then(|s| s.to_str()).unwrap_or("Book1").to_string();
|
let stem = std::path::Path::new(&title).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Book1").to_string();
|
||||||
if let Some(pick) = &app.services.pick_save {
|
if let Some(pick) = &app.services.pick_save {
|
||||||
let Some(path) = pick(&format!("{stem}.{fmt}")) else { return false };
|
let Some(path) = pick(&format!("{stem}.{fmt}")) else { return false };
|
||||||
// The picker may suggest a workbook extension; the export writes the chosen format.
|
// The picker may suggest a workbook extension; the export writes the chosen format.
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ impl Dialog {
|
|||||||
// The file rides along in the values (`path` on the desktop, `name` and `base64`
|
// The file rides along in the values (`path` on the desktop, `name` and `base64`
|
||||||
// on the web) and goes back to `file.open` with the password.
|
// on the web) and goes back to `file.open` with the password.
|
||||||
let source = p("path").as_str().or(p("name").as_str()).map(str::to_string).unwrap_or_default();
|
let source = p("path").as_str().or(p("name").as_str()).map(str::to_string).unwrap_or_default();
|
||||||
let file = std::path::Path::new(&source).file_name().and_then(|n| n.to_str()).unwrap_or("This workbook").to_string();
|
let file = std::path::Path::new(&source).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("This workbook").to_string();
|
||||||
// An empty password from the start, so OK on the untouched field says "incorrect"
|
// An empty password from the start, so OK on the untouched field says "incorrect"
|
||||||
// rather than asking for the password again.
|
// rather than asking for the password again.
|
||||||
let mut values = params.clone();
|
let mut values = params.clone();
|
||||||
@@ -1369,7 +1369,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
|||||||
&& let Some(path) = pick()
|
&& let Some(path) = pick()
|
||||||
{
|
{
|
||||||
d.values.remove("base64");
|
d.values.remove("base64");
|
||||||
let alt = std::path::Path::new(&path).file_name().and_then(|name| name.to_str()).unwrap_or("");
|
let alt = std::path::Path::new(&path).file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("");
|
||||||
d.values.insert("alt".into(), json!(alt));
|
d.values.insert("alt".into(), json!(alt));
|
||||||
d.values.insert("path".into(), json!(path));
|
d.values.insert("path".into(), json!(path));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub fn system_font_dirs() -> Vec<PathBuf> {
|
|||||||
/// Weight implied by a style name (`"Bold"` → 700, `"Light"` → 300).
|
/// Weight implied by a style name (`"Bold"` → 700, `"Light"` → 300).
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn style_weight(style: &str) -> f32 {
|
fn style_weight(style: &str) -> f32 {
|
||||||
let s: String = style.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect();
|
let s: String = style.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect();
|
||||||
const TABLE: &[(&str, f32)] = &[
|
const TABLE: &[(&str, f32)] = &[
|
||||||
("hairline", 100.0),
|
("hairline", 100.0),
|
||||||
("thin", 100.0),
|
("thin", 100.0),
|
||||||
@@ -107,7 +107,7 @@ impl Face {
|
|||||||
/// Whether a style name reads as italic or oblique.
|
/// Whether a style name reads as italic or oblique.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn style_italic(style: &str) -> bool {
|
fn style_italic(style: &str) -> bool {
|
||||||
let s: String = style.chars().filter(|c| c.is_alphanumeric()).flat_map(|c| c.to_lowercase()).collect();
|
let s: String = style.chars().filter(|c| c.is_alphanumeric()).flat_map(char::to_lowercase).collect();
|
||||||
s.contains("italic") || s.contains("oblique")
|
s.contains("italic") || s.contains("oblique")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +165,10 @@ pub fn scan_dirs(dirs: &[PathBuf]) -> Vec<Face> {
|
|||||||
stack.push(path);
|
stack.push(path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let is_font =
|
let is_font = path
|
||||||
path.extension().and_then(|e| e.to_str()).is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"));
|
.extension()
|
||||||
|
.and_then(std::ffi::OsStr::to_str)
|
||||||
|
.is_some_and(|e| matches!(e.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc" | "otc"));
|
||||||
if !is_font || std::fs::metadata(&path).map(|m| m.len() > MAX_FONT_FILE).unwrap_or(true) {
|
if !is_font || std::fs::metadata(&path).map(|m| m.len() > MAX_FONT_FILE).unwrap_or(true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -366,12 +368,12 @@ fn bound() -> &'static std::sync::RwLock<HashSet<String>> {
|
|||||||
|
|
||||||
/// Record the role families a freshly built [`egui::FontDefinitions`] bound.
|
/// Record the role families a freshly built [`egui::FontDefinitions`] bound.
|
||||||
pub fn set_bound(keys: impl IntoIterator<Item = String>) {
|
pub fn set_bound(keys: impl IntoIterator<Item = String>) {
|
||||||
*bound().write().unwrap_or_else(|e| e.into_inner()) = keys.into_iter().collect();
|
*bound().write().unwrap_or_else(std::sync::PoisonError::into_inner) = keys.into_iter().collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Is this exact role family bound to font data?
|
/// Is this exact role family bound to font data?
|
||||||
pub fn is_bound(role: &str) -> bool {
|
pub fn is_bound(role: &str) -> bool {
|
||||||
bound().read().unwrap_or_else(|e| e.into_inner()).contains(role)
|
bound().read().unwrap_or_else(std::sync::PoisonError::into_inner).contains(role)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serializes tests that mutate the process-wide bound-family registry.
|
/// Serializes tests that mutate the process-wide bound-family registry.
|
||||||
|
|||||||
@@ -2200,9 +2200,8 @@ fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
app.selected_chart = None;
|
|
||||||
}
|
}
|
||||||
|
app.selected_chart = None;
|
||||||
}
|
}
|
||||||
// A right-click on a chart, picture, shape or slicer opens the object's menu, not the cell's
|
// A right-click on a chart, picture, shape or slicer opens the object's menu, not the cell's
|
||||||
// under it; a right press that wobbles into a drag counts too (#2067).
|
// under it; a right press that wobbles into a drag counts too (#2067).
|
||||||
|
|||||||
@@ -567,7 +567,7 @@ impl SheetApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let title = self.session.active().map(|d| d.display_title()).unwrap_or_else(|| "Book1".into());
|
let title = self.session.active().map(|d| d.display_title()).unwrap_or_else(|| "Book1".into());
|
||||||
let stem = std::path::Path::new(&title).file_stem().and_then(|s| s.to_str()).unwrap_or("Book1").to_string();
|
let stem = std::path::Path::new(&title).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Book1").to_string();
|
||||||
let Some(pick) = &self.services.pick_save else { return };
|
let Some(pick) = &self.services.pick_save else { return };
|
||||||
let Some(path) = pick(&format!("{stem}.pdf")) else { return };
|
let Some(path) = pick(&format!("{stem}.pdf")) else { return };
|
||||||
// The picker may suggest a workbook extension; the export writes a PDF.
|
// The picker may suggest a workbook extension; the export writes a PDF.
|
||||||
|
|||||||
+162
-190
@@ -14,6 +14,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use egui::{Color32, FontData, FontDefinitions, FontFamily, FontId, Stroke, Visuals};
|
use egui::{Color32, FontData, FontDefinitions, FontFamily, FontId, Stroke, Visuals};
|
||||||
|
|
||||||
|
/// The light or dark value of a colour, by mode.
|
||||||
|
fn pick<T>(dark: bool, light: T, dark_value: T) -> T {
|
||||||
|
if dark { dark_value } else { light }
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct Tokens {
|
pub struct Tokens {
|
||||||
pub dark: bool,
|
pub dark: bool,
|
||||||
@@ -129,110 +134,70 @@ const SHEET_TRACK: Color32 = rgb(0xD6D6D6);
|
|||||||
|
|
||||||
impl Tokens {
|
impl Tokens {
|
||||||
pub fn light() -> Tokens {
|
pub fn light() -> Tokens {
|
||||||
Tokens {
|
Tokens::palette(false)
|
||||||
dark: false,
|
|
||||||
window: Color32::from_rgb(0xF3, 0xF3, 0xF3),
|
|
||||||
ribbon: Color32::from_rgb(0xFF, 0xFF, 0xFF),
|
|
||||||
ribbon_border: Color32::from_rgb(0xE3, 0xE3, 0xE3),
|
|
||||||
text: Color32::from_rgb(0x24, 0x24, 0x24),
|
|
||||||
text_dim: Color32::from_rgb(0x61, 0x61, 0x61),
|
|
||||||
text_disabled: Color32::from_rgb(0xB0, 0xB0, 0xB0),
|
|
||||||
accent: Color32::from_rgb(0x10, 0x7C, 0x41),
|
|
||||||
accent_dark: Color32::from_rgb(0x0B, 0x5E, 0x31),
|
|
||||||
accent_soft: Color32::from_rgb(0xDF, 0xF1, 0xE6),
|
|
||||||
hover: Color32::from_rgb(0xEB, 0xEB, 0xEB),
|
|
||||||
pressed: Color32::from_rgb(0xDD, 0xDD, 0xDD),
|
|
||||||
separator: Color32::from_rgb(0xE0, 0xE0, 0xE0),
|
|
||||||
grid_bg: Color32::WHITE,
|
|
||||||
desk: Color32::from_rgb(0xDE, 0xDE, 0xDE),
|
|
||||||
gridline: Color32::from_rgb(0xE1, 0xE1, 0xE1),
|
|
||||||
header_bg: Color32::from_rgb(0xF8, 0xF8, 0xF8),
|
|
||||||
header_text: Color32::from_rgb(0x42, 0x42, 0x42),
|
|
||||||
header_line: Color32::from_rgb(0xD6, 0xD6, 0xD6),
|
|
||||||
header_sel_bg: Color32::from_rgb(0xE1, 0xE1, 0xE1),
|
|
||||||
header_sel_text: Color32::from_rgb(0x0B, 0x5E, 0x31),
|
|
||||||
header_all_bg: Color32::from_rgb(0xC8, 0xE6, 0xD3),
|
|
||||||
sel_fill: Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x1C),
|
|
||||||
sel_border: Color32::from_rgb(0x10, 0x7C, 0x41),
|
|
||||||
input_bg: Color32::WHITE,
|
|
||||||
input_border: Color32::from_rgb(0xD1, 0xD1, 0xD1),
|
|
||||||
tab_bar: Color32::from_rgb(0xF3, 0xF3, 0xF3),
|
|
||||||
tab_active: Color32::WHITE,
|
|
||||||
status_bar: Color32::from_rgb(0xF3, 0xF3, 0xF3),
|
|
||||||
cell_text: Color32::BLACK,
|
|
||||||
menu_bg: Color32::WHITE,
|
|
||||||
shadow: Color32::from_black_alpha(40),
|
|
||||||
danger: Color32::from_rgb(0xC4, 0x2B, 0x1C),
|
|
||||||
error_check_green: Color32::from_rgb(0x1E, 0x8E, 0x3E),
|
|
||||||
warning_fill: Color32::from_rgb(0xF2, 0xB7, 0x05),
|
|
||||||
warning_ink: Color32::BLACK,
|
|
||||||
notice_warn_bg: Color32::from_rgb(0xFF, 0xF4, 0xCE),
|
|
||||||
notice_warn_border: Color32::from_rgb(0xF0, 0xC3, 0x6E),
|
|
||||||
notice_info_bg: Color32::from_rgb(0xEA, 0xF2, 0xFB),
|
|
||||||
notice_info_border: Color32::from_rgb(0xB9, 0xD2, 0xEC),
|
|
||||||
on_accent: Color32::WHITE,
|
|
||||||
cat: [rgb(0x2F6FCF), rgb(0x1E8A4C), rgb(0xC8601A), rgb(0xC8372D), rgb(0x8350C8), rgb(0x13808A), rgb(0x9C6C00), rgb(0x5A6B7D)],
|
|
||||||
paper: Color32::WHITE,
|
|
||||||
ref_colors: SHEET_REF_COLORS,
|
|
||||||
sheet_ref_colors: SHEET_REF_COLORS,
|
|
||||||
syntax: LIGHT_SYNTAX,
|
|
||||||
warning: rgb(0xF2C32E),
|
|
||||||
on_warning: rgb(0x1F1F1F),
|
|
||||||
sheet_control_bg: SHEET_CONTROL_BG,
|
|
||||||
sheet_control_border: SHEET_CONTROL_BORDER,
|
|
||||||
sheet_control_text: SHEET_CONTROL_TEXT,
|
|
||||||
sheet_track: SHEET_TRACK,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dark() -> Tokens {
|
pub fn dark() -> Tokens {
|
||||||
|
Tokens::palette(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One palette for both modes: each colour is written once, with its light and dark values.
|
||||||
|
fn palette(dark: bool) -> Tokens {
|
||||||
Tokens {
|
Tokens {
|
||||||
dark: true,
|
dark,
|
||||||
window: Color32::from_rgb(0x29, 0x29, 0x29),
|
window: pick(dark, Color32::from_rgb(0xF3, 0xF3, 0xF3), Color32::from_rgb(0x29, 0x29, 0x29)),
|
||||||
ribbon: Color32::from_rgb(0x33, 0x33, 0x33),
|
ribbon: pick(dark, Color32::from_rgb(0xFF, 0xFF, 0xFF), Color32::from_rgb(0x33, 0x33, 0x33)),
|
||||||
ribbon_border: Color32::from_rgb(0x40, 0x40, 0x40),
|
ribbon_border: pick(dark, Color32::from_rgb(0xE3, 0xE3, 0xE3), Color32::from_rgb(0x40, 0x40, 0x40)),
|
||||||
text: Color32::from_rgb(0xEE, 0xEE, 0xEE),
|
text: pick(dark, Color32::from_rgb(0x24, 0x24, 0x24), Color32::from_rgb(0xEE, 0xEE, 0xEE)),
|
||||||
text_dim: Color32::from_rgb(0xB0, 0xB0, 0xB0),
|
text_dim: pick(dark, Color32::from_rgb(0x61, 0x61, 0x61), Color32::from_rgb(0xB0, 0xB0, 0xB0)),
|
||||||
text_disabled: Color32::from_rgb(0x70, 0x70, 0x70),
|
text_disabled: pick(dark, Color32::from_rgb(0xB0, 0xB0, 0xB0), Color32::from_rgb(0x70, 0x70, 0x70)),
|
||||||
accent: Color32::from_rgb(0x5C, 0xC5, 0x87),
|
accent: pick(dark, Color32::from_rgb(0x10, 0x7C, 0x41), Color32::from_rgb(0x5C, 0xC5, 0x87)),
|
||||||
accent_dark: Color32::from_rgb(0x3F, 0xA8, 0x6C),
|
accent_dark: pick(dark, Color32::from_rgb(0x0B, 0x5E, 0x31), Color32::from_rgb(0x3F, 0xA8, 0x6C)),
|
||||||
accent_soft: Color32::from_rgb(0x23, 0x45, 0x31),
|
accent_soft: pick(dark, Color32::from_rgb(0xDF, 0xF1, 0xE6), Color32::from_rgb(0x23, 0x45, 0x31)),
|
||||||
hover: Color32::from_rgb(0x42, 0x42, 0x42),
|
hover: pick(dark, Color32::from_rgb(0xEB, 0xEB, 0xEB), Color32::from_rgb(0x42, 0x42, 0x42)),
|
||||||
pressed: Color32::from_rgb(0x4D, 0x4D, 0x4D),
|
pressed: pick(dark, Color32::from_rgb(0xDD, 0xDD, 0xDD), Color32::from_rgb(0x4D, 0x4D, 0x4D)),
|
||||||
separator: Color32::from_rgb(0x48, 0x48, 0x48),
|
separator: pick(dark, Color32::from_rgb(0xE0, 0xE0, 0xE0), Color32::from_rgb(0x48, 0x48, 0x48)),
|
||||||
grid_bg: Color32::WHITE,
|
grid_bg: Color32::WHITE,
|
||||||
desk: Color32::from_rgb(0x3A, 0x3A, 0x3A),
|
desk: pick(dark, Color32::from_rgb(0xDE, 0xDE, 0xDE), Color32::from_rgb(0x3A, 0x3A, 0x3A)),
|
||||||
gridline: Color32::from_rgb(0xE1, 0xE1, 0xE1),
|
gridline: Color32::from_rgb(0xE1, 0xE1, 0xE1),
|
||||||
header_bg: Color32::from_rgb(0x30, 0x30, 0x30),
|
header_bg: pick(dark, Color32::from_rgb(0xF8, 0xF8, 0xF8), Color32::from_rgb(0x30, 0x30, 0x30)),
|
||||||
header_text: Color32::from_rgb(0xCC, 0xCC, 0xCC),
|
header_text: pick(dark, Color32::from_rgb(0x42, 0x42, 0x42), Color32::from_rgb(0xCC, 0xCC, 0xCC)),
|
||||||
header_line: Color32::from_rgb(0x45, 0x45, 0x45),
|
header_line: pick(dark, Color32::from_rgb(0xD6, 0xD6, 0xD6), Color32::from_rgb(0x45, 0x45, 0x45)),
|
||||||
header_sel_bg: Color32::from_rgb(0x45, 0x45, 0x45),
|
header_sel_bg: pick(dark, Color32::from_rgb(0xE1, 0xE1, 0xE1), Color32::from_rgb(0x45, 0x45, 0x45)),
|
||||||
header_sel_text: Color32::from_rgb(0x6F, 0xD3, 0x9A),
|
header_sel_text: pick(dark, Color32::from_rgb(0x0B, 0x5E, 0x31), Color32::from_rgb(0x6F, 0xD3, 0x9A)),
|
||||||
header_all_bg: Color32::from_rgb(0x2E, 0x55, 0x3E),
|
header_all_bg: pick(dark, Color32::from_rgb(0xC8, 0xE6, 0xD3), Color32::from_rgb(0x2E, 0x55, 0x3E)),
|
||||||
sel_fill: Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x24),
|
sel_fill: pick(dark, Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x1C), Color32::from_rgba_unmultiplied(0x10, 0x7C, 0x41, 0x24)),
|
||||||
sel_border: Color32::from_rgb(0x10, 0x7C, 0x41),
|
sel_border: Color32::from_rgb(0x10, 0x7C, 0x41),
|
||||||
input_bg: Color32::from_rgb(0x1F, 0x1F, 0x1F),
|
input_bg: pick(dark, Color32::WHITE, Color32::from_rgb(0x1F, 0x1F, 0x1F)),
|
||||||
input_border: Color32::from_rgb(0x50, 0x50, 0x50),
|
input_border: pick(dark, Color32::from_rgb(0xD1, 0xD1, 0xD1), Color32::from_rgb(0x50, 0x50, 0x50)),
|
||||||
tab_bar: Color32::from_rgb(0x29, 0x29, 0x29),
|
tab_bar: pick(dark, Color32::from_rgb(0xF3, 0xF3, 0xF3), Color32::from_rgb(0x29, 0x29, 0x29)),
|
||||||
tab_active: Color32::from_rgb(0x3A, 0x3A, 0x3A),
|
tab_active: pick(dark, Color32::WHITE, Color32::from_rgb(0x3A, 0x3A, 0x3A)),
|
||||||
status_bar: Color32::from_rgb(0x29, 0x29, 0x29),
|
status_bar: pick(dark, Color32::from_rgb(0xF3, 0xF3, 0xF3), Color32::from_rgb(0x29, 0x29, 0x29)),
|
||||||
cell_text: Color32::BLACK,
|
cell_text: Color32::BLACK,
|
||||||
menu_bg: Color32::from_rgb(0x30, 0x30, 0x30),
|
menu_bg: pick(dark, Color32::WHITE, Color32::from_rgb(0x30, 0x30, 0x30)),
|
||||||
shadow: Color32::from_black_alpha(90),
|
shadow: pick(dark, Color32::from_black_alpha(40), Color32::from_black_alpha(90)),
|
||||||
danger: Color32::from_rgb(0xFF, 0x95, 0x85),
|
danger: pick(dark, Color32::from_rgb(0xC4, 0x2B, 0x1C), Color32::from_rgb(0xFF, 0x95, 0x85)),
|
||||||
error_check_green: Color32::from_rgb(0x4C, 0xC2, 0x6B),
|
error_check_green: pick(dark, Color32::from_rgb(0x1E, 0x8E, 0x3E), Color32::from_rgb(0x4C, 0xC2, 0x6B)),
|
||||||
warning_fill: Color32::from_rgb(0xF2, 0xB7, 0x05),
|
warning_fill: Color32::from_rgb(0xF2, 0xB7, 0x05),
|
||||||
warning_ink: Color32::BLACK,
|
warning_ink: Color32::BLACK,
|
||||||
notice_warn_bg: Color32::from_rgb(0x4A, 0x3F, 0x1E),
|
notice_warn_bg: pick(dark, Color32::from_rgb(0xFF, 0xF4, 0xCE), Color32::from_rgb(0x4A, 0x3F, 0x1E)),
|
||||||
notice_warn_border: Color32::from_rgb(0x7A, 0x67, 0x30),
|
notice_warn_border: pick(dark, Color32::from_rgb(0xF0, 0xC3, 0x6E), Color32::from_rgb(0x7A, 0x67, 0x30)),
|
||||||
notice_info_bg: Color32::from_rgb(0x24, 0x33, 0x44),
|
notice_info_bg: pick(dark, Color32::from_rgb(0xEA, 0xF2, 0xFB), Color32::from_rgb(0x24, 0x33, 0x44)),
|
||||||
notice_info_border: Color32::from_rgb(0x3B, 0x55, 0x72),
|
notice_info_border: pick(dark, Color32::from_rgb(0xB9, 0xD2, 0xEC), Color32::from_rgb(0x3B, 0x55, 0x72)),
|
||||||
on_accent: rgb(0x0F2A1A),
|
on_accent: pick(dark, Color32::WHITE, rgb(0x0F2A1A)),
|
||||||
cat: [rgb(0x6EA8F7), rgb(0x5CC587), rgb(0xF0965A), rgb(0xF07B6E), rgb(0xB394EE), rgb(0x4CC7CF), rgb(0xF2C14E), rgb(0xA9B6C4)],
|
cat: pick(
|
||||||
paper: rgb(0x454545),
|
dark,
|
||||||
ref_colors: [rgb(0x6FA3F5), rgb(0xF27C7C), rgb(0xB394EE), rgb(0x6CCB85), rgb(0xF0A061), rgb(0x5CC9DA), rgb(0xE77FC4), rgb(0xD4C25A)],
|
[rgb(0x2F6FCF), rgb(0x1E8A4C), rgb(0xC8601A), rgb(0xC8372D), rgb(0x8350C8), rgb(0x13808A), rgb(0x9C6C00), rgb(0x5A6B7D)],
|
||||||
|
[rgb(0x6EA8F7), rgb(0x5CC587), rgb(0xF0965A), rgb(0xF07B6E), rgb(0xB394EE), rgb(0x4CC7CF), rgb(0xF2C14E), rgb(0xA9B6C4)],
|
||||||
|
),
|
||||||
|
paper: pick(dark, Color32::WHITE, rgb(0x454545)),
|
||||||
|
ref_colors: pick(
|
||||||
|
dark,
|
||||||
|
SHEET_REF_COLORS,
|
||||||
|
[rgb(0x6FA3F5), rgb(0xF27C7C), rgb(0xB394EE), rgb(0x6CCB85), rgb(0xF0A061), rgb(0x5CC9DA), rgb(0xE77FC4), rgb(0xD4C25A)],
|
||||||
|
),
|
||||||
sheet_ref_colors: SHEET_REF_COLORS,
|
sheet_ref_colors: SHEET_REF_COLORS,
|
||||||
syntax: DARK_SYNTAX,
|
syntax: pick(dark, LIGHT_SYNTAX, DARK_SYNTAX),
|
||||||
warning: rgb(0xF2C32E),
|
warning: rgb(0xF2C32E),
|
||||||
on_warning: rgb(0x1F1F1F),
|
on_warning: rgb(0x1F1F1F),
|
||||||
sheet_control_bg: SHEET_CONTROL_BG,
|
sheet_control_bg: SHEET_CONTROL_BG,
|
||||||
@@ -382,7 +347,7 @@ fn try_load(paths: &[(&str, u32)]) -> Option<FontData> {
|
|||||||
/// looked for again next time.
|
/// looked for again next time.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn font_file(path: &str, index: u32) -> Option<&'static [u8]> {
|
fn font_file(path: &str, index: u32) -> Option<&'static [u8]> {
|
||||||
static FILES: std::sync::Mutex<std::collections::BTreeMap<String, &'static [u8]>> = std::sync::Mutex::new(std::collections::BTreeMap::new());
|
static FILES: std::sync::Mutex<std::collections::BTreeMap<String, &[u8]>> = std::sync::Mutex::new(std::collections::BTreeMap::new());
|
||||||
let mut files = FILES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
let mut files = FILES.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
if let Some(&bytes) = files.get(path) {
|
if let Some(&bytes) = files.get(path) {
|
||||||
return font_parses(bytes, index).then_some(bytes);
|
return font_parses(bytes, index).then_some(bytes);
|
||||||
@@ -422,6 +387,12 @@ fn home_fonts(file: &str) -> String {
|
|||||||
format!("{home}/Library/Fonts/{file}")
|
format!("{home}/Library/Fonts/{file}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every file in `files` under every directory in `dirs`, directory first, in the same order a
|
||||||
|
/// nested loop over the directories would add them.
|
||||||
|
fn in_dirs(dirs: &[&str], files: &[&str]) -> Vec<String> {
|
||||||
|
dirs.iter().flat_map(|d| files.iter().map(move |f| format!("{d}/{f}"))).collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Candidate font files per role, in preference order.
|
/// Candidate font files per role, in preference order.
|
||||||
fn candidates(role: &str) -> Vec<(String, u32)> {
|
fn candidates(role: &str) -> Vec<(String, u32)> {
|
||||||
let mac_sys = "/System/Library/Fonts";
|
let mac_sys = "/System/Library/Fonts";
|
||||||
@@ -436,11 +407,11 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||||||
add(format!("{mac_sys}/SFNS.ttf"), 0);
|
add(format!("{mac_sys}/SFNS.ttf"), 0);
|
||||||
add(format!("{mac_sys}/HelveticaNeue.ttc"), 0);
|
add(format!("{mac_sys}/HelveticaNeue.ttc"), 0);
|
||||||
add(format!("{win}\\segoeui.ttf"), 0);
|
add(format!("{win}\\segoeui.ttf"), 0);
|
||||||
for d in lin {
|
for p in in_dirs(
|
||||||
add(format!("{d}/cantarell/Cantarell-Regular.otf"), 0);
|
&lin,
|
||||||
add(format!("{d}/noto/NotoSans-Regular.ttf"), 0);
|
&["cantarell/Cantarell-Regular.otf", "noto/NotoSans-Regular.ttf", "dejavu/DejaVuSans.ttf", "liberation/LiberationSans-Regular.ttf"],
|
||||||
add(format!("{d}/dejavu/DejaVuSans.ttf"), 0);
|
) {
|
||||||
add(format!("{d}/liberation/LiberationSans-Regular.ttf"), 0);
|
add(p, 0);
|
||||||
}
|
}
|
||||||
// Fedora keeps each family in its own package folder (#54).
|
// Fedora keeps each family in its own package folder (#54).
|
||||||
add("/usr/share/fonts/abattis-cantarell-fonts/Cantarell-Regular.otf".into(), 0);
|
add("/usr/share/fonts/abattis-cantarell-fonts/Cantarell-Regular.otf".into(), 0);
|
||||||
@@ -451,10 +422,8 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||||||
"ui-bold" => {
|
"ui-bold" => {
|
||||||
add(format!("{mac_sys}/HelveticaNeue.ttc"), 1);
|
add(format!("{mac_sys}/HelveticaNeue.ttc"), 1);
|
||||||
add(format!("{win}\\segoeuib.ttf"), 0);
|
add(format!("{win}\\segoeuib.ttf"), 0);
|
||||||
for d in lin {
|
for p in in_dirs(&lin, &["noto/NotoSans-Bold.ttf", "dejavu/DejaVuSans-Bold.ttf", "liberation/LiberationSans-Bold.ttf"]) {
|
||||||
add(format!("{d}/noto/NotoSans-Bold.ttf"), 0);
|
add(p, 0);
|
||||||
add(format!("{d}/dejavu/DejaVuSans-Bold.ttf"), 0);
|
|
||||||
add(format!("{d}/liberation/LiberationSans-Bold.ttf"), 0);
|
|
||||||
}
|
}
|
||||||
add("/usr/share/fonts/google-noto/NotoSans-Bold.ttf".into(), 0);
|
add("/usr/share/fonts/google-noto/NotoSans-Bold.ttf".into(), 0);
|
||||||
add("/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf".into(), 0);
|
add("/usr/share/fonts/dejavu-sans-fonts/DejaVuSans-Bold.ttf".into(), 0);
|
||||||
@@ -497,18 +466,16 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||||||
add(format!("/Library/Fonts/{calibri_mac}"), 0);
|
add(format!("/Library/Fonts/{calibri_mac}"), 0);
|
||||||
add(home_fonts(calibri_mac), 0);
|
add(home_fonts(calibri_mac), 0);
|
||||||
add(format!("{win}\\{calibri_win}"), 0);
|
add(format!("{win}\\{calibri_win}"), 0);
|
||||||
for d in lin {
|
for p in in_dirs(&lin, &[&format!("crosextra/{carlito}"), &format!("carlito/{carlito}")]) {
|
||||||
add(format!("{d}/crosextra/{carlito}"), 0);
|
add(p, 0);
|
||||||
add(format!("{d}/carlito/{carlito}"), 0);
|
|
||||||
}
|
}
|
||||||
// Fedora keeps each family in its own package folder, as for the UI fonts (#54).
|
// Fedora keeps each family in its own package folder, as for the UI fonts (#54).
|
||||||
add(format!("/usr/share/fonts/google-carlito-fonts/{carlito}"), 0);
|
add(format!("/usr/share/fonts/google-carlito-fonts/{carlito}"), 0);
|
||||||
add(home_fonts(carlito), 0);
|
add(home_fonts(carlito), 0);
|
||||||
add(format!("{mac_sup}/{arial_mac}"), 0);
|
add(format!("{mac_sup}/{arial_mac}"), 0);
|
||||||
add(format!("{win}\\{arial_win}"), 0);
|
add(format!("{win}\\{arial_win}"), 0);
|
||||||
for d in lin {
|
for p in in_dirs(&lin, &[&format!("liberation/{lib}"), &format!("dejavu/{dejavu}")]) {
|
||||||
add(format!("{d}/liberation/{lib}"), 0);
|
add(p, 0);
|
||||||
add(format!("{d}/dejavu/{dejavu}"), 0);
|
|
||||||
}
|
}
|
||||||
add(format!("/usr/share/fonts/liberation-sans-fonts/{lib}"), 0);
|
add(format!("/usr/share/fonts/liberation-sans-fonts/{lib}"), 0);
|
||||||
add(format!("/usr/share/fonts/dejavu-sans-fonts/{dejavu}"), 0);
|
add(format!("/usr/share/fonts/dejavu-sans-fonts/{dejavu}"), 0);
|
||||||
@@ -516,9 +483,8 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||||||
"serif" => {
|
"serif" => {
|
||||||
add(format!("{mac_sup}/Times New Roman.ttf"), 0);
|
add(format!("{mac_sup}/Times New Roman.ttf"), 0);
|
||||||
add(format!("{win}\\times.ttf"), 0);
|
add(format!("{win}\\times.ttf"), 0);
|
||||||
for d in lin {
|
for p in in_dirs(&lin, &["liberation/LiberationSerif-Regular.ttf", "dejavu/DejaVuSerif.ttf"]) {
|
||||||
add(format!("{d}/liberation/LiberationSerif-Regular.ttf"), 0);
|
add(p, 0);
|
||||||
add(format!("{d}/dejavu/DejaVuSerif.ttf"), 0);
|
|
||||||
}
|
}
|
||||||
add("/usr/share/fonts/liberation-serif-fonts/LiberationSerif-Regular.ttf".into(), 0);
|
add("/usr/share/fonts/liberation-serif-fonts/LiberationSerif-Regular.ttf".into(), 0);
|
||||||
add("/usr/share/fonts/dejavu-serif-fonts/DejaVuSerif.ttf".into(), 0);
|
add("/usr/share/fonts/dejavu-serif-fonts/DejaVuSerif.ttf".into(), 0);
|
||||||
@@ -527,14 +493,14 @@ fn candidates(role: &str) -> Vec<(String, u32)> {
|
|||||||
add(format!("{mac_sys}/SFNSMono.ttf"), 0);
|
add(format!("{mac_sys}/SFNSMono.ttf"), 0);
|
||||||
add(format!("{mac_sys}/Menlo.ttc"), 0);
|
add(format!("{mac_sys}/Menlo.ttc"), 0);
|
||||||
add(format!("{win}\\consola.ttf"), 0);
|
add(format!("{win}\\consola.ttf"), 0);
|
||||||
for d in lin {
|
for p in in_dirs(&lin, &["dejavu/DejaVuSansMono.ttf"]) {
|
||||||
add(format!("{d}/dejavu/DejaVuSansMono.ttf"), 0);
|
add(p, 0);
|
||||||
}
|
}
|
||||||
add("/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf".into(), 0);
|
add("/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf".into(), 0);
|
||||||
// Fedora installs DejaVu Sans Mono only on request. Liberation Mono comes with LibreOffice
|
// Fedora installs DejaVu Sans Mono only on request. Liberation Mono comes with LibreOffice
|
||||||
// (in Workstation and KDE by default), Noto Sans Mono (variable, Regular by default) always.
|
// (in Workstation and KDE by default), Noto Sans Mono (variable, Regular by default) always.
|
||||||
for d in lin {
|
for p in in_dirs(&lin, &["liberation/LiberationMono-Regular.ttf"]) {
|
||||||
add(format!("{d}/liberation/LiberationMono-Regular.ttf"), 0);
|
add(p, 0);
|
||||||
}
|
}
|
||||||
add("/usr/share/fonts/liberation-mono-fonts/LiberationMono-Regular.ttf".into(), 0);
|
add("/usr/share/fonts/liberation-mono-fonts/LiberationMono-Regular.ttf".into(), 0);
|
||||||
add("/usr/share/fonts/google-noto-vf/NotoSansMono[wght].ttf".into(), 0);
|
add("/usr/share/fonts/google-noto-vf/NotoSansMono[wght].ttf".into(), 0);
|
||||||
@@ -725,6 +691,24 @@ const LINUX_CJK_DIRS: [&str; 22] = [
|
|||||||
"/usr/share/fonts/chromeos/notocjk",
|
"/usr/share/fonts/chromeos/notocjk",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// The Windows fonts folder (`%WINDIR%\Fonts`, or the default install location).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn windows_fonts() -> String {
|
||||||
|
std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Noto Sans CJK faces for one locale (`jp`, `sc`, `kr`): the per-locale OTF, then the
|
||||||
|
/// region-merged TTC at `face`, in every Linux font directory.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn noto_cjk(locale: &str, face: u32) -> Vec<(String, u32)> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for d in LINUX_CJK_DIRS {
|
||||||
|
v.push((format!("{d}/NotoSansCJK{locale}-Regular.otf"), 0));
|
||||||
|
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), face));
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
/// Japanese Han + Kana faces (Japanese kanji glyph shapes).
|
/// Japanese Han + Kana faces (Japanese kanji glyph shapes).
|
||||||
///
|
///
|
||||||
/// The region-merged `NotoSansCJK-Regular.ttc` holds one face per locale (JP 0, KR 1, SC 2, TC 3,
|
/// The region-merged `NotoSansCJK-Regular.ttc` holds one face per locale (JP 0, KR 1, SC 2, TC 3,
|
||||||
@@ -733,15 +717,12 @@ const LINUX_CJK_DIRS: [&str; 22] = [
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn cjk_han_japanese() -> Vec<(String, u32)> {
|
fn cjk_han_japanese() -> Vec<(String, u32)> {
|
||||||
let mac = "/System/Library/Fonts";
|
let mac = "/System/Library/Fonts";
|
||||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
let win = windows_fonts();
|
||||||
let mut v: Vec<(String, u32)> = Vec::new();
|
let mut v: Vec<(String, u32)> = Vec::new();
|
||||||
v.push((format!("{win}\\YuGothR.ttc"), 0)); // Yu Gothic
|
v.push((format!("{win}\\YuGothR.ttc"), 0)); // Yu Gothic
|
||||||
v.push((format!("{win}\\msgothic.ttc"), 0)); // MS Gothic
|
v.push((format!("{win}\\msgothic.ttc"), 0)); // MS Gothic
|
||||||
v.push((format!("{mac}/ヒラギノ角ゴシック W3.ttc"), 0)); // Hiragino Sans
|
v.push((format!("{mac}/ヒラギノ角ゴシック W3.ttc"), 0)); // Hiragino Sans
|
||||||
for d in LINUX_CJK_DIRS {
|
v.extend(noto_cjk("jp", 0));
|
||||||
v.push((format!("{d}/NotoSansCJKjp-Regular.otf"), 0));
|
|
||||||
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 0)); // JP face
|
|
||||||
}
|
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,17 +731,15 @@ fn cjk_han_japanese() -> Vec<(String, u32)> {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn cjk_han_simplified() -> Vec<(String, u32)> {
|
fn cjk_han_simplified() -> Vec<(String, u32)> {
|
||||||
let mac = "/System/Library/Fonts";
|
let mac = "/System/Library/Fonts";
|
||||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
let win = windows_fonts();
|
||||||
let mut v: Vec<(String, u32)> = Vec::new();
|
let mut v: Vec<(String, u32)> = vec![
|
||||||
v.push((format!("{win}\\msyh.ttc"), 0)); // Microsoft YaHei
|
(format!("{win}\\msyh.ttc"), 0), // Microsoft YaHei
|
||||||
v.push((format!("{win}\\simsun.ttc"), 0)); // SimSun
|
(format!("{win}\\simsun.ttc"), 0), // SimSun
|
||||||
v.push((format!("{win}\\simhei.ttf"), 0)); // SimHei
|
(format!("{win}\\simhei.ttf"), 0), // SimHei
|
||||||
v.push((format!("{mac}/Hiragino Sans GB.ttc"), 0)); // ships on every recent macOS
|
(format!("{mac}/Hiragino Sans GB.ttc"), 0), // ships on every recent macOS
|
||||||
v.push((format!("{mac}/PingFang.ttc"), 0)); // older macOS only; now a downloadable asset
|
(format!("{mac}/PingFang.ttc"), 0), // older macOS only; now a downloadable asset
|
||||||
for d in LINUX_CJK_DIRS {
|
];
|
||||||
v.push((format!("{d}/NotoSansCJKsc-Regular.otf"), 0));
|
v.extend(noto_cjk("sc", 2));
|
||||||
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 2)); // SC face
|
|
||||||
}
|
|
||||||
for d in LINUX_CJK_DIRS {
|
for d in LINUX_CJK_DIRS {
|
||||||
v.push((format!("{d}/wqy/wqy-microhei.ttc"), 0));
|
v.push((format!("{d}/wqy/wqy-microhei.ttc"), 0));
|
||||||
v.push((format!("{d}/wenquanyi/wqy-microhei.ttc"), 0));
|
v.push((format!("{d}/wenquanyi/wqy-microhei.ttc"), 0));
|
||||||
@@ -774,59 +753,65 @@ fn cjk_han_simplified() -> Vec<(String, u32)> {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn cjk_hangul() -> Vec<(String, u32)> {
|
fn cjk_hangul() -> Vec<(String, u32)> {
|
||||||
let mac = "/System/Library/Fonts";
|
let mac = "/System/Library/Fonts";
|
||||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
let win = windows_fonts();
|
||||||
let mut v: Vec<(String, u32)> = Vec::new();
|
let mut v: Vec<(String, u32)> = Vec::new();
|
||||||
v.push((format!("{win}\\malgun.ttf"), 0)); // Malgun Gothic
|
v.push((format!("{win}\\malgun.ttf"), 0)); // Malgun Gothic
|
||||||
v.push((format!("{mac}/AppleSDGothicNeo.ttc"), 0));
|
v.push((format!("{mac}/AppleSDGothicNeo.ttc"), 0));
|
||||||
for d in LINUX_CJK_DIRS {
|
v.extend(noto_cjk("kr", 1));
|
||||||
v.push((format!("{d}/NotoSansCJKkr-Regular.otf"), 0));
|
|
||||||
v.push((format!("{d}/NotoSansCJK-Regular.ttc"), 1)); // KR face
|
|
||||||
}
|
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Thai faces. Windows ships Leelawadee UI and Tahoma/Microsoft Sans Serif (both carry Thai);
|
/// Thai faces. Windows ships Leelawadee UI and Tahoma/Microsoft Sans Serif (both carry Thai);
|
||||||
/// macOS has Thonburi; Linux usually has Noto Sans Thai or Garuda.
|
/// Linux font directories searched by the Thai and Hebrew faces, in priority order.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn thai() -> Vec<(String, u32)> {
|
fn linux_noto_dirs() -> [&'static str; 5] {
|
||||||
let mac_sys = "/System/Library/Fonts";
|
[
|
||||||
let mac_sup = "/System/Library/Fonts/Supplemental";
|
|
||||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
|
||||||
let lin = [
|
|
||||||
"/usr/share/fonts/truetype/noto",
|
"/usr/share/fonts/truetype/noto",
|
||||||
"/usr/share/fonts/opentype/noto",
|
"/usr/share/fonts/opentype/noto",
|
||||||
"/usr/share/fonts/truetype",
|
"/usr/share/fonts/truetype",
|
||||||
"/usr/share/fonts",
|
"/usr/share/fonts",
|
||||||
"/usr/share/fonts/chromeos/noto", // ChromeOS's Linux container (#1762)
|
"/usr/share/fonts/chromeos/noto", // ChromeOS's Linux container (#1762)
|
||||||
];
|
]
|
||||||
let mut v: Vec<(String, u32)> = Vec::new();
|
}
|
||||||
v.push((format!("{win}\\LeelawUI.ttf"), 0)); // Leelawadee UI
|
|
||||||
v.push((format!("{win}\\tahoma.ttf"), 0)); // Tahoma
|
/// Faces for one script: Windows files under `WINDIR\Fonts` first, then macOS paths, then the
|
||||||
v.push((format!("{win}\\micross.ttf"), 0)); // Microsoft Sans Serif
|
/// Noto/Linux files in every [`linux_noto_dirs`] directory, then the Fedora variable font.
|
||||||
v.push((format!("{mac_sup}/Thonburi.ttc"), 0));
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
v.push((format!("{mac_sys}/SukhumvitSet.ttc"), 0));
|
fn script_fonts(win: &[&str], mac: &[&str], lin_files: &[&str], variable: &str) -> Vec<(String, u32)> {
|
||||||
for d in lin {
|
let w = windows_fonts();
|
||||||
v.push((format!("{d}/NotoSansThai-Regular.ttf"), 0));
|
let mut v: Vec<(String, u32)> = win.iter().map(|f| (format!("{w}\\{f}"), 0)).collect();
|
||||||
v.push((format!("{d}/tlwg/Garuda.ttf"), 0));
|
v.extend(mac.iter().map(|p| (p.to_string(), 0)));
|
||||||
v.push((format!("{d}/garuda/Garuda.ttf"), 0));
|
v.extend(in_dirs(&linux_noto_dirs(), lin_files).into_iter().map(|p| (p, 0)));
|
||||||
}
|
v.push((variable.into(), 0));
|
||||||
// Fedora ships the variable Noto Sans Thai; its default instance is Regular.
|
|
||||||
v.push(("/usr/share/fonts/google-noto-vf/NotoSansThai[wght].ttf".into(), 0));
|
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// macOS has Thonburi; Linux usually has Noto Sans Thai or Garuda.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn thai() -> Vec<(String, u32)> {
|
||||||
|
script_fonts(
|
||||||
|
// Leelawadee UI, Tahoma, Microsoft Sans Serif
|
||||||
|
&["LeelawUI.ttf", "tahoma.ttf", "micross.ttf"],
|
||||||
|
&["/System/Library/Fonts/Supplemental/Thonburi.ttc", "/System/Library/Fonts/SukhumvitSet.ttc"],
|
||||||
|
&["NotoSansThai-Regular.ttf", "tlwg/Garuda.ttf", "garuda/Garuda.ttf"],
|
||||||
|
// Fedora ships the variable Noto Sans Thai; its default instance is Regular.
|
||||||
|
"/usr/share/fonts/google-noto-vf/NotoSansThai[wght].ttf",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Lao faces. Windows ships Leelawadee UI (and DokChampa on older versions); macOS has Lao Sangam
|
/// Lao faces. Windows ships Leelawadee UI (and DokChampa on older versions); macOS has Lao Sangam
|
||||||
/// MN and Lao MN; Linux usually has Noto Sans Lao or Phetsarath OT.
|
/// MN and Lao MN; Linux usually has Noto Sans Lao or Phetsarath OT.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn lao() -> Vec<(String, u32)> {
|
fn lao() -> Vec<(String, u32)> {
|
||||||
let mac_sup = "/System/Library/Fonts/Supplemental";
|
let mac_sup = "/System/Library/Fonts/Supplemental";
|
||||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
let win = windows_fonts();
|
||||||
let lin = ["/usr/share/fonts/truetype/noto", "/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
|
let lin = ["/usr/share/fonts/truetype/noto", "/usr/share/fonts/opentype/noto", "/usr/share/fonts/truetype", "/usr/share/fonts"];
|
||||||
let mut v: Vec<(String, u32)> = Vec::new();
|
let mut v: Vec<(String, u32)> = vec![
|
||||||
v.push((format!("{win}\\LeelawUI.ttf"), 0)); // Leelawadee UI
|
(format!("{win}\\LeelawUI.ttf"), 0), // Leelawadee UI
|
||||||
v.push((format!("{win}\\dokchamp.ttf"), 0)); // DokChampa
|
(format!("{win}\\dokchamp.ttf"), 0), // DokChampa
|
||||||
v.push((format!("{mac_sup}/Lao Sangam MN.ttf"), 0));
|
(format!("{mac_sup}/Lao Sangam MN.ttf"), 0),
|
||||||
v.push((format!("{mac_sup}/Lao MN.ttc"), 0));
|
(format!("{mac_sup}/Lao MN.ttc"), 0),
|
||||||
|
];
|
||||||
for d in lin {
|
for d in lin {
|
||||||
v.push((format!("{d}/NotoSansLao-Regular.ttf"), 0));
|
v.push((format!("{d}/NotoSansLao-Regular.ttf"), 0));
|
||||||
v.push((format!("{d}/lao/Phetsarath_OT.ttf"), 0));
|
v.push((format!("{d}/lao/Phetsarath_OT.ttf"), 0));
|
||||||
@@ -839,33 +824,20 @@ fn lao() -> Vec<(String, u32)> {
|
|||||||
/// Arial Hebrew and David; Linux usually has Noto Sans Hebrew or DejaVu Sans.
|
/// Arial Hebrew and David; Linux usually has Noto Sans Hebrew or DejaVu Sans.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn hebrew() -> Vec<(String, u32)> {
|
fn hebrew() -> Vec<(String, u32)> {
|
||||||
let mac_sys = "/System/Library/Fonts";
|
script_fonts(
|
||||||
let mac_sup = "/System/Library/Fonts/Supplemental";
|
// David, FrankRuehl, Tahoma
|
||||||
let win = std::env::var("WINDIR").map(|w| format!("{w}\\Fonts")).unwrap_or_else(|_| "C:\\Windows\\Fonts".into());
|
&["david.ttf", "frank.ttf", "tahoma.ttf"],
|
||||||
let lin = [
|
&[
|
||||||
"/usr/share/fonts/truetype/noto",
|
"/System/Library/Fonts/ArialHB.ttc",
|
||||||
"/usr/share/fonts/opentype/noto",
|
"/System/Library/Fonts/SFHebrew.ttf",
|
||||||
"/usr/share/fonts/truetype",
|
"/System/Library/Fonts/Supplemental/Arial Hebrew.ttf",
|
||||||
"/usr/share/fonts",
|
"/System/Library/Fonts/Supplemental/David.ttf",
|
||||||
"/usr/share/fonts/chromeos/noto", // ChromeOS's Linux container (#1762)
|
"/System/Library/Fonts/LucidaGrande.ttc",
|
||||||
];
|
"/System/Library/Fonts/Lucida Grande.ttc",
|
||||||
let mut v: Vec<(String, u32)> = Vec::new();
|
],
|
||||||
v.push((format!("{win}\\david.ttf"), 0)); // David
|
&["NotoSansHebrew-Regular.ttf", "dejavu/DejaVuSans.ttf", "freefont/FreeSans.ttf"],
|
||||||
v.push((format!("{win}\\frank.ttf"), 0)); // FrankRuehl
|
"/usr/share/fonts/google-noto-vf/NotoSansHebrew[wght].ttf",
|
||||||
v.push((format!("{win}\\tahoma.ttf"), 0)); // Tahoma
|
)
|
||||||
v.push((format!("{mac_sys}/ArialHB.ttc"), 0)); // Arial Hebrew
|
|
||||||
v.push((format!("{mac_sys}/SFHebrew.ttf"), 0));
|
|
||||||
v.push((format!("{mac_sup}/Arial Hebrew.ttf"), 0));
|
|
||||||
v.push((format!("{mac_sup}/David.ttf"), 0));
|
|
||||||
v.push((format!("{mac_sys}/LucidaGrande.ttc"), 0));
|
|
||||||
v.push((format!("{mac_sys}/Lucida Grande.ttc"), 0));
|
|
||||||
for d in lin {
|
|
||||||
v.push((format!("{d}/NotoSansHebrew-Regular.ttf"), 0));
|
|
||||||
v.push((format!("{d}/dejavu/DejaVuSans.ttf"), 0));
|
|
||||||
v.push((format!("{d}/freefont/FreeSans.ttf"), 0));
|
|
||||||
}
|
|
||||||
v.push(("/usr/share/fonts/google-noto-vf/NotoSansHebrew[wght].ttf".into(), 0));
|
|
||||||
v
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Symbol faces behind the UI font. Interface fonts such as Noto Sans and Cantarell (and egui's
|
/// Symbol faces behind the UI font. Interface fonts such as Noto Sans and Cantarell (and egui's
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
//! The open, solid polylines the model can represent as editable ink. This is DrawingML
|
//! The open, solid polylines the model can represent as editable ink. This is DrawingML
|
||||||
//! geometry, not Office InkML. Unsupported geometry keeps the existing shape-import fallback.
|
//! geometry, not Office InkML. Unsupported geometry keeps the existing shape-import fallback.
|
||||||
|
|
||||||
use super::*;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
use gridcraft_model::{Anchor, Color, Shape, ShapeKind, Theme};
|
||||||
|
|
||||||
|
use super::xfrm;
|
||||||
|
use crate::tables::EMU_PER_PX;
|
||||||
|
use crate::xml::El;
|
||||||
|
|
||||||
const MAX_POINTS: usize = 20_000;
|
const MAX_POINTS: usize = 20_000;
|
||||||
const MAX_COORD: f64 = 1e7;
|
const MAX_COORD: f64 = 1e7;
|
||||||
@@ -24,9 +30,7 @@ fn one<'a>(e: &'a El, name: &str) -> Option<&'a El> {
|
|||||||
found.next().is_none().then_some(first)
|
found.next().is_none().then_some(first)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Do not partially import a path: losing a curve, subpath, transform or line effect changes
|
fn validate_shape_obj(obj: &El) -> Option<&El> {
|
||||||
/// the drawing. Accept only the explicit subset emitted below, including its line styling.
|
|
||||||
pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
|
||||||
if obj.name != "sp"
|
if obj.name != "sp"
|
||||||
|| !only_children(obj, &["nvSpPr", "spPr"])
|
|| !only_children(obj, &["nvSpPr", "spPr"])
|
||||||
|| !only_attrs(obj, &["macro", "textlink"])
|
|| !only_attrs(obj, &["macro", "textlink"])
|
||||||
@@ -43,6 +47,10 @@ pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
|||||||
if !no_fill.attrs.is_empty() || !no_fill.children.is_empty() {
|
if !no_fill.attrs.is_empty() || !no_fill.children.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
Some(sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_xfrm(sp: &El, anchor: &Anchor) -> Option<()> {
|
||||||
let xf = one(sp, "xfrm")?;
|
let xf = one(sp, "xfrm")?;
|
||||||
if !xf.attrs.is_empty() || !only_children(xf, &["off", "ext"]) {
|
if !xf.attrs.is_empty() || !only_children(xf, &["off", "ext"]) {
|
||||||
return None;
|
return None;
|
||||||
@@ -62,6 +70,10 @@ pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
|||||||
if !same_extent(ew, anchor.width) || !same_extent(eh, anchor.height) {
|
if !same_extent(ew, anchor.width) || !same_extent(eh, anchor.height) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_cust_geom(sp: &El) -> Option<(&El, i64, i64)> {
|
||||||
let geom = one(sp, "custGeom")?;
|
let geom = one(sp, "custGeom")?;
|
||||||
if !geom.attrs.is_empty() || !only_children(geom, &["avLst", "gdLst", "ahLst", "cxnLst", "rect", "pathLst"]) {
|
if !geom.attrs.is_empty() || !only_children(geom, &["avLst", "gdLst", "ahLst", "cxnLst", "rect", "pathLst"]) {
|
||||||
return None;
|
return None;
|
||||||
@@ -90,6 +102,10 @@ pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
|||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
Some((path, w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_line(sp: &El) -> Option<(i64, Color)> {
|
||||||
let ln = one(sp, "ln")?;
|
let ln = one(sp, "ln")?;
|
||||||
if !only_attrs(ln, &["w", "cap", "cmpd", "algn"])
|
if !only_attrs(ln, &["w", "cap", "cmpd", "algn"])
|
||||||
|| ln.attr("cap") != Some("flat")
|
|| ln.attr("cap") != Some("flat")
|
||||||
@@ -129,6 +145,10 @@ pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let line = Color::from_hex(color.attr("val")?)?;
|
let line = Color::from_hex(color.attr("val")?)?;
|
||||||
|
Some((width, line))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_points(path: &El, anchor: &Anchor, w: i64, h: i64) -> Option<String> {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
for (i, command) in path.children.iter().enumerate() {
|
for (i, command) in path.children.iter().enumerate() {
|
||||||
if command.name != if i == 0 { "moveTo" } else { "lnTo" } || !command.attrs.is_empty() || command.children.len() != 1 {
|
if command.name != if i == 0 { "moveTo" } else { "lnTo" } || !command.attrs.is_empty() || command.children.len() != 1 {
|
||||||
@@ -148,6 +168,17 @@ pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
|||||||
}
|
}
|
||||||
let _ = write!(text, "{},{}", x as f32, y as f32);
|
let _ = write!(text, "{},{}", x as f32, y as f32);
|
||||||
}
|
}
|
||||||
|
Some(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do not partially import a path: losing a curve, subpath, transform or line effect changes
|
||||||
|
/// the drawing. Accept only the explicit subset emitted below, including its line styling.
|
||||||
|
pub(super) fn read(obj: &El, anchor: Anchor) -> Option<Shape> {
|
||||||
|
let sp = validate_shape_obj(obj)?;
|
||||||
|
validate_xfrm(sp, &anchor)?;
|
||||||
|
let (path, w, h) = validate_cust_geom(sp)?;
|
||||||
|
let (width, line) = validate_line(sp)?;
|
||||||
|
let text = parse_points(path, &anchor, w, h)?;
|
||||||
Some(Shape { id: 0, kind: ShapeKind::Ink, anchor, fill: Color::Rgb((width as u32) << 16), line, text, format: Default::default() })
|
Some(Shape { id: 0, kind: ShapeKind::Ink, anchor, fill: Color::Rgb((width as u32) << 16), line, text, format: Default::default() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ pub enum XlsxKind {
|
|||||||
impl XlsxKind {
|
impl XlsxKind {
|
||||||
/// The kind a file name's extension asks for; anything else is a plain workbook.
|
/// The kind a file name's extension asks for; anything else is a plain workbook.
|
||||||
pub fn from_path(path: &str) -> XlsxKind {
|
pub fn from_path(path: &str) -> XlsxKind {
|
||||||
let ext = std::path::Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or_default().to_ascii_lowercase();
|
let ext = std::path::Path::new(path).extension().and_then(std::ffi::OsStr::to_str).unwrap_or_default().to_ascii_lowercase();
|
||||||
match ext.as_str() {
|
match ext.as_str() {
|
||||||
"xlsm" => XlsxKind::MacroWorkbook,
|
"xlsm" => XlsxKind::MacroWorkbook,
|
||||||
"xltx" => XlsxKind::Template,
|
"xltx" => XlsxKind::Template,
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ fn group_items(sys: DateSystem, by: PivotDateGroup, start: f64, end: f64) -> Vec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
PivotDateGroup::Quarters => v.extend((1..=4).map(|q| format!("Qtr{q}"))),
|
PivotDateGroup::Quarters => v.extend((1..=4).map(|q| format!("Qtr{q}"))),
|
||||||
PivotDateGroup::Months => v.extend(MONTHS.iter().map(|m| m.to_string())),
|
PivotDateGroup::Months => v.extend(MONTHS.iter().map(ToString::to_string)),
|
||||||
PivotDateGroup::Days | PivotDateGroup::None => {
|
PivotDateGroup::Days | PivotDateGroup::None => {
|
||||||
const DAYS: [u32; 12] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
const DAYS: [u32; 12] = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||||
for (m, n) in MONTHS.iter().zip(DAYS) {
|
for (m, n) in MONTHS.iter().zip(DAYS) {
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ pub fn encode_xstring(s: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn looks_like_escape(chars: &[char], i: usize) -> bool {
|
fn looks_like_escape(chars: &[char], i: usize) -> bool {
|
||||||
chars.get(i + 1) == Some(&'x') && chars.get(i + 6) == Some(&'_') && (i + 2..i + 6).all(|k| chars.get(k).is_some_and(|c| c.is_ascii_hexdigit()))
|
chars.get(i + 1) == Some(&'x') && chars.get(i + 6) == Some(&'_') && (i + 2..i + 6).all(|k| chars.get(k).is_some_and(char::is_ascii_hexdigit))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn escape_unit(chars: &[char], i: usize) -> Option<u32> {
|
fn escape_unit(chars: &[char], i: usize) -> Option<u32> {
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ const EXAMPLE_EXT: &[&str] = &["csv", "tsv", "json"];
|
|||||||
const ASSET_DIRS: &[&str] = &["assets/", "docs/brand/", "docs/images/"];
|
const ASSET_DIRS: &[&str] = &["assets/", "docs/brand/", "docs/images/"];
|
||||||
|
|
||||||
fn ext(path: &str) -> String {
|
fn ext(path: &str) -> String {
|
||||||
Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default()
|
Path::new(path).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_asset(path: &str) -> bool {
|
pub fn is_asset(path: &str) -> bool {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ pub fn run(root: &Path) -> Result<(), String> {
|
|||||||
};
|
};
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let Some(stem) = path.file_stem().and_then(|s| s.to_str()).map(str::to_owned) else { continue };
|
let Some(stem) = path.file_stem().and_then(std::ffi::OsStr::to_str).map(str::to_owned) else { continue };
|
||||||
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
|
if path.extension().and_then(std::ffi::OsStr::to_str) != Some("rs") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Ok(source) = std::fs::read_to_string(&path) else { continue };
|
let Ok(source) = std::fs::read_to_string(&path) else { continue };
|
||||||
|
|||||||
+1
-1
@@ -174,7 +174,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