mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 11:46:49 -06:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8938fdfdcf | ||
|
|
55ff9d9f54 |
+60
-53
@@ -927,6 +927,64 @@ impl Host for PassHost<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Strongly connected components of a graph given as adjacency lists (Tarjan, iterative): each
|
||||
/// node's component index, and the number of nodes in each component.
|
||||
fn components(edges: &[Vec<usize>]) -> (Vec<usize>, Vec<usize>) {
|
||||
let n = edges.len();
|
||||
let (mut low, mut num) = (vec![0usize; n], vec![usize::MAX; n]);
|
||||
let (mut on_stack, mut stack, mut comp) = (vec![false; n], Vec::new(), vec![usize::MAX; n]);
|
||||
let (mut next, mut comps) = (0usize, 0usize);
|
||||
for root in 0..n {
|
||||
if num[root] != usize::MAX {
|
||||
continue;
|
||||
}
|
||||
let mut call: Vec<(usize, usize)> = vec![(root, 0)];
|
||||
num[root] = next;
|
||||
low[root] = next;
|
||||
next += 1;
|
||||
stack.push(root);
|
||||
on_stack[root] = true;
|
||||
while let Some(top) = call.last_mut() {
|
||||
let v = top.0;
|
||||
if let Some(&w) = edges.get(v).and_then(|e| e.get(top.1)) {
|
||||
top.1 += 1;
|
||||
if num[w] == usize::MAX {
|
||||
num[w] = next;
|
||||
low[w] = next;
|
||||
next += 1;
|
||||
stack.push(w);
|
||||
on_stack[w] = true;
|
||||
call.push((w, 0));
|
||||
} else if on_stack[w] {
|
||||
low[v] = low[v].min(num[w]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
call.pop();
|
||||
if let Some(&(u, _)) = call.last() {
|
||||
low[u] = low[u].min(low[v]);
|
||||
}
|
||||
if low[v] == num[v] {
|
||||
while let Some(w) = stack.pop() {
|
||||
on_stack[w] = false;
|
||||
comp[w] = comps;
|
||||
if w == v {
|
||||
break;
|
||||
}
|
||||
}
|
||||
comps += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut size = vec![0usize; comps];
|
||||
for &c in &comp {
|
||||
if let Some(s) = size.get_mut(c) {
|
||||
*s += 1;
|
||||
}
|
||||
}
|
||||
(comp, size)
|
||||
}
|
||||
|
||||
impl Calc {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
@@ -981,59 +1039,8 @@ impl Calc {
|
||||
e
|
||||
})
|
||||
.collect();
|
||||
// Tarjan's strongly connected components, iterative.
|
||||
let n = circ.len();
|
||||
let (mut low, mut num) = (vec![0usize; n], vec![usize::MAX; n]);
|
||||
let (mut on_stack, mut stack, mut comp) = (vec![false; n], Vec::new(), vec![usize::MAX; n]);
|
||||
let (mut next, mut comps) = (0usize, 0usize);
|
||||
for root in 0..n {
|
||||
if num[root] != usize::MAX {
|
||||
continue;
|
||||
}
|
||||
let mut call: Vec<(usize, usize)> = vec![(root, 0)];
|
||||
num[root] = next;
|
||||
low[root] = next;
|
||||
next += 1;
|
||||
stack.push(root);
|
||||
on_stack[root] = true;
|
||||
while let Some(top) = call.last_mut() {
|
||||
let v = top.0;
|
||||
if let Some(&w) = edges.get(v).and_then(|e| e.get(top.1)) {
|
||||
top.1 += 1;
|
||||
if num[w] == usize::MAX {
|
||||
num[w] = next;
|
||||
low[w] = next;
|
||||
next += 1;
|
||||
stack.push(w);
|
||||
on_stack[w] = true;
|
||||
call.push((w, 0));
|
||||
} else if on_stack[w] {
|
||||
low[v] = low[v].min(num[w]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
call.pop();
|
||||
if let Some(&(u, _)) = call.last() {
|
||||
low[u] = low[u].min(low[v]);
|
||||
}
|
||||
if low[v] == num[v] {
|
||||
while let Some(w) = stack.pop() {
|
||||
on_stack[w] = false;
|
||||
comp[w] = comps;
|
||||
if w == v {
|
||||
break;
|
||||
}
|
||||
}
|
||||
comps += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut size = vec![0usize; comps];
|
||||
for &c in &comp {
|
||||
if let Some(s) = size.get_mut(c) {
|
||||
*s += 1;
|
||||
}
|
||||
}
|
||||
// Loop members: nodes in a component of more than one, or with an edge to themselves.
|
||||
let (comp, size) = components(&edges);
|
||||
let on_loop = |i: usize| comp.get(i).and_then(|c| size.get(*c)).is_some_and(|s| *s > 1) || edges.get(i).is_some_and(|e| e.contains(&i));
|
||||
// A formula with dynamic references (INDIRECT, OFFSET) may close a loop the graph can't see.
|
||||
let mut out: Vec<Key> =
|
||||
|
||||
@@ -96,75 +96,73 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
]
|
||||
}
|
||||
|
||||
fn auto_sum(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let func = str_param(p, "function").unwrap_or("SUM").to_ascii_uppercase();
|
||||
let d = s.doc()?;
|
||||
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
|
||||
let sel = match str_param(p, "range") {
|
||||
Some(_) => target_range(s, p)?,
|
||||
None => d.selection.current(),
|
||||
};
|
||||
if !sel.is_single() {
|
||||
// A selection: totals go in its empty last row or column, as Excel's do, and otherwise
|
||||
// past it: to the right of a single row, below each column of anything taller. Columns
|
||||
// past the sheet's data get no total (a whole-row selection doesn't write 16,384
|
||||
// formulas), nor do rows past it, and all totals are one edit (one undo step).
|
||||
let Some(used) = sh.cells.used_range() else { return ok() };
|
||||
let empty = |row: u32, col: u32| sh.value(CellRef::new(row, col)).is_empty();
|
||||
let sum = |from: CellRef, to: CellRef| format!("={func}({})", RangeRef::new(from, to).a1());
|
||||
let last_col = used.end.col.min(sel.end.col);
|
||||
let mut writes: Vec<(CellRef, String)> = Vec::new();
|
||||
if sel.start.row == sel.end.row {
|
||||
// One row: its total goes in its last cell when that is empty, else just after it.
|
||||
let row = sel.start.row;
|
||||
let end_col = if sel.is_full_rows() { last_col } else { sel.end.col };
|
||||
let (data_end, out) = if end_col > sel.start.col && empty(row, end_col) { (end_col - 1, end_col) } else { (end_col, end_col + 1) };
|
||||
if out >= gridcraft_core::MAX_COLS {
|
||||
return Err(EngineError::Other("There's no room to the right of the selection for the total.".into()));
|
||||
}
|
||||
writes.push((CellRef::new(row, out), sum(CellRef::new(row, sel.start.col), CellRef::new(row, data_end))));
|
||||
} else {
|
||||
// An empty last column takes each row's total; rows past the data get none.
|
||||
let rows_end = sel.end.row.min(used.end.row);
|
||||
let right_blank = !sel.is_full_rows() && sel.end.col > sel.start.col && (sel.start.row..=rows_end).all(|r| empty(r, sel.end.col));
|
||||
let data_end_col = if right_blank { sel.end.col - 1 } else { last_col };
|
||||
let last_row_blank = (sel.start.col..=last_col).all(|c| empty(sel.end.row, c));
|
||||
let (data_end_row, out_row) = if last_row_blank { (sel.end.row.saturating_sub(1), sel.end.row) } else { (sel.end.row, sel.end.row + 1) };
|
||||
// Each column's total, below it: unless only the last column is there to take totals.
|
||||
if last_row_blank || !right_blank {
|
||||
if out_row >= gridcraft_core::MAX_ROWS {
|
||||
return Err(EngineError::Other("There's no room below the selection for the totals.".into()));
|
||||
}
|
||||
for c in sel.start.col..=data_end_col.min(last_col) {
|
||||
writes.push((CellRef::new(out_row, c), sum(CellRef::new(sel.start.row, c), CellRef::new(data_end_row, c))));
|
||||
}
|
||||
}
|
||||
if right_blank {
|
||||
for r in sel.start.row..=data_end_row.min(rows_end) {
|
||||
writes.push((CellRef::new(r, sel.end.col), sum(CellRef::new(r, sel.start.col), CellRef::new(r, data_end_col))));
|
||||
}
|
||||
// Both empty: the corner takes the total of the whole block.
|
||||
if last_row_blank {
|
||||
writes.push((
|
||||
CellRef::new(sel.end.row, sel.end.col),
|
||||
sum(CellRef::new(sel.start.row, sel.start.col), CellRef::new(data_end_row, data_end_col)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
let sheet = d.wb.active_sheet;
|
||||
edit(s, |cx| {
|
||||
for (c, f) in &writes {
|
||||
let cell = super::edit::input_to_cell(f, sheet, *c, &mut cx.wb)?.unwrap_or_default();
|
||||
cx.sheet_mut(sheet)?.set_cell(*c, cell);
|
||||
cx.touch(sheet, *c);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
return ok();
|
||||
/// Whether a cell on the active sheet is empty.
|
||||
fn blank(sh: &gridcraft_model::sheet::Sheet, row: u32, col: u32) -> bool {
|
||||
sh.value(CellRef::new(row, col)).is_empty()
|
||||
}
|
||||
|
||||
fn sum_formula(func: &str, from: CellRef, to: CellRef) -> String {
|
||||
format!("={func}({})", RangeRef::new(from, to).a1())
|
||||
}
|
||||
|
||||
/// Totals for a multi-cell selection as `(cell, formula)` writes. `None` when the sheet has no data.
|
||||
fn selection_totals(sh: &gridcraft_model::sheet::Sheet, sel: RangeRef, func: &str) -> Result<Option<Vec<(CellRef, String)>>> {
|
||||
// A selection: totals go in its empty last row or column, as Excel's do, and otherwise
|
||||
// past it: to the right of a single row, below each column of anything taller. Columns
|
||||
// past the sheet's data get no total (a whole-row selection doesn't write 16,384
|
||||
// formulas), nor do rows past it, and all totals are one edit (one undo step).
|
||||
let Some(used) = sh.cells.used_range() else { return Ok(None) };
|
||||
if sel.start.row == sel.end.row { single_row_totals(sh, sel, used, func).map(Some) } else { block_totals(sh, sel, used, func).map(Some) }
|
||||
}
|
||||
|
||||
/// One row: its total goes in its last cell when that is empty, else just after it.
|
||||
fn single_row_totals(sh: &gridcraft_model::sheet::Sheet, sel: RangeRef, used: RangeRef, func: &str) -> Result<Vec<(CellRef, String)>> {
|
||||
let row = sel.start.row;
|
||||
let last_col = used.end.col.min(sel.end.col);
|
||||
let end_col = if sel.is_full_rows() { last_col } else { sel.end.col };
|
||||
let (data_end, out) = if end_col > sel.start.col && blank(sh, row, end_col) { (end_col - 1, end_col) } else { (end_col, end_col + 1) };
|
||||
if out >= gridcraft_core::MAX_COLS {
|
||||
return Err(EngineError::Other("There's no room to the right of the selection for the total.".into()));
|
||||
}
|
||||
// Single cell: guess the range above, else to the left.
|
||||
let at = sel.start;
|
||||
Ok(vec![(CellRef::new(row, out), sum_formula(func, CellRef::new(row, sel.start.col), CellRef::new(row, data_end)))])
|
||||
}
|
||||
|
||||
/// A block of rows: each column's total below it, and each row's total in an empty last column.
|
||||
fn block_totals(sh: &gridcraft_model::sheet::Sheet, sel: RangeRef, used: RangeRef, func: &str) -> Result<Vec<(CellRef, String)>> {
|
||||
let mut writes: Vec<(CellRef, String)> = Vec::new();
|
||||
let last_col = used.end.col.min(sel.end.col);
|
||||
// An empty last column takes each row's total; rows past the data get none.
|
||||
let rows_end = sel.end.row.min(used.end.row);
|
||||
let right_blank = !sel.is_full_rows() && sel.end.col > sel.start.col && (sel.start.row..=rows_end).all(|r| blank(sh, r, sel.end.col));
|
||||
let data_end_col = if right_blank { sel.end.col - 1 } else { last_col };
|
||||
let last_row_blank = (sel.start.col..=last_col).all(|c| blank(sh, sel.end.row, c));
|
||||
let (data_end_row, out_row) = if last_row_blank { (sel.end.row.saturating_sub(1), sel.end.row) } else { (sel.end.row, sel.end.row + 1) };
|
||||
// Each column's total, below it: unless only the last column is there to take totals.
|
||||
if last_row_blank || !right_blank {
|
||||
if out_row >= gridcraft_core::MAX_ROWS {
|
||||
return Err(EngineError::Other("There's no room below the selection for the totals.".into()));
|
||||
}
|
||||
for c in sel.start.col..=data_end_col.min(last_col) {
|
||||
writes.push((CellRef::new(out_row, c), sum_formula(func, CellRef::new(sel.start.row, c), CellRef::new(data_end_row, c))));
|
||||
}
|
||||
}
|
||||
if right_blank {
|
||||
for r in sel.start.row..=data_end_row.min(rows_end) {
|
||||
writes.push((CellRef::new(r, sel.end.col), sum_formula(func, CellRef::new(r, sel.start.col), CellRef::new(r, data_end_col))));
|
||||
}
|
||||
// Both empty: the corner takes the total of the whole block.
|
||||
if last_row_blank {
|
||||
writes.push((
|
||||
CellRef::new(sel.end.row, sel.end.col),
|
||||
sum_formula(func, CellRef::new(sel.start.row, sel.start.col), CellRef::new(data_end_row, data_end_col)),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(writes)
|
||||
}
|
||||
|
||||
/// The range a single cell's AutoSum guesses: the numbers above it, else the numbers to its left.
|
||||
fn guessed_range(sh: &gridcraft_model::sheet::Sheet, at: CellRef) -> Option<RangeRef> {
|
||||
let numeric = |c: CellRef| sh.value(c).is_number();
|
||||
let mut range = None;
|
||||
if at.row > 0 && (numeric(CellRef::new(at.row - 1, at.col)) || at.col == 0 || !numeric(CellRef::new(at.row, at.col - 1))) {
|
||||
@@ -186,7 +184,33 @@ fn auto_sum(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
}
|
||||
range = Some(RangeRef::new(CellRef::new(at.row, left), CellRef::new(at.row, at.col - 1)));
|
||||
}
|
||||
let text = match range {
|
||||
range
|
||||
}
|
||||
|
||||
fn auto_sum(s: &mut Session, p: &Json) -> Result<Json> {
|
||||
let func = str_param(p, "function").unwrap_or("SUM").to_ascii_uppercase();
|
||||
let d = s.doc()?;
|
||||
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
|
||||
let sel = match str_param(p, "range") {
|
||||
Some(_) => target_range(s, p)?,
|
||||
None => d.selection.current(),
|
||||
};
|
||||
if !sel.is_single() {
|
||||
let Some(writes) = selection_totals(sh, sel, &func)? else { return ok() };
|
||||
let sheet = d.wb.active_sheet;
|
||||
edit(s, |cx| {
|
||||
for (c, f) in &writes {
|
||||
let cell = super::edit::input_to_cell(f, sheet, *c, &mut cx.wb)?.unwrap_or_default();
|
||||
cx.sheet_mut(sheet)?.set_cell(*c, cell);
|
||||
cx.touch(sheet, *c);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
return ok();
|
||||
}
|
||||
// Single cell: guess the range above, else to the left.
|
||||
let at = sel.start;
|
||||
let text = match guessed_range(sh, at) {
|
||||
Some(r) => format!("={func}({})", r.a1()),
|
||||
None => {
|
||||
s.ui_requests.push(crate::UiRequest::EditCell(Some(format!("={func}()"))));
|
||||
|
||||
@@ -957,6 +957,35 @@ pub fn export_as(app: &mut SheetApp, fmt: &str) -> bool {
|
||||
fn options(app: &mut SheetApp, ui: &mut Ui) {
|
||||
let lang = app.ui.language;
|
||||
section(ui, lang, "Options|General");
|
||||
general_options(app, ui, lang);
|
||||
ui.add_space(6.0);
|
||||
let mut formula_bar = app.session.prefs.formula_bar;
|
||||
if ui.checkbox(&mut formula_bar, lang.tr("Show formula bar")).changed() {
|
||||
app.run_or_alert("view.formulaBar", json!({"on": formula_bar}));
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Options|Editing");
|
||||
let mut autocomplete = app.ui.autocomplete;
|
||||
if ui.checkbox(&mut autocomplete, lang.tr("Enable AutoComplete for cell values")).changed() {
|
||||
app.run_or_alert("options.autoComplete", json!({"on": autocomplete}));
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Options|Save");
|
||||
let mut autosave = app.ui.autosave;
|
||||
if ui.checkbox(&mut autosave, lang.tr("Save .xlsx workbooks automatically after every change")).changed() {
|
||||
app.toggle_autosave();
|
||||
}
|
||||
let Some(calc) = app.session.active().map(|d| d.wb.calc.clone()) else { return };
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Formulas");
|
||||
ui.label(lang.tr("Workbook calculation"));
|
||||
calc_options(app, ui, lang, &calc);
|
||||
ui.add_space(6.0);
|
||||
dim(ui, lang.tr("Calculation settings are saved with the workbook."));
|
||||
}
|
||||
|
||||
/// The display theme and interface language controls of the General section.
|
||||
fn general_options(app: &mut SheetApp, ui: &mut Ui, lang: Language) {
|
||||
egui::Grid::new("backstage_options_general").num_columns(2).spacing(vec2(18.0, 10.0)).show(ui, |ui| {
|
||||
let label = ui.label(lang.tr("Display theme"));
|
||||
let mode = app.ui.theme_mode();
|
||||
@@ -993,27 +1022,10 @@ fn options(app: &mut SheetApp, ui: &mut Ui) {
|
||||
.labelled_by(label.id);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let mut formula_bar = app.session.prefs.formula_bar;
|
||||
if ui.checkbox(&mut formula_bar, lang.tr("Show formula bar")).changed() {
|
||||
app.run_or_alert("view.formulaBar", json!({"on": formula_bar}));
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Options|Editing");
|
||||
let mut autocomplete = app.ui.autocomplete;
|
||||
if ui.checkbox(&mut autocomplete, lang.tr("Enable AutoComplete for cell values")).changed() {
|
||||
app.run_or_alert("options.autoComplete", json!({"on": autocomplete}));
|
||||
}
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Options|Save");
|
||||
let mut autosave = app.ui.autosave;
|
||||
if ui.checkbox(&mut autosave, lang.tr("Save .xlsx workbooks automatically after every change")).changed() {
|
||||
app.toggle_autosave();
|
||||
}
|
||||
let Some(calc) = app.session.active().map(|d| d.wb.calc.clone()) else { return };
|
||||
ui.add_space(18.0);
|
||||
section(ui, lang, "Formulas");
|
||||
ui.label(lang.tr("Workbook calculation"));
|
||||
}
|
||||
|
||||
/// The calculation mode, and iterative calculation, of the active workbook.
|
||||
fn calc_options(app: &mut SheetApp, ui: &mut Ui, lang: Language, calc: &gridcraft_engine::model::CalcSettings) {
|
||||
use gridcraft_engine::model::CalcMode;
|
||||
for (mode, id, label) in [
|
||||
(CalcMode::Automatic, "automatic", "Automatic"),
|
||||
@@ -1030,6 +1042,13 @@ fn options(app: &mut SheetApp, ui: &mut Ui) {
|
||||
app.run_or_alert("formulas.calculationOptions", json!({"iterative": iterative}));
|
||||
}
|
||||
// Maximum Iterations and Maximum Change apply only while iteration is on, as in Excel.
|
||||
iteration_options(app, ui, lang, calc, iterative);
|
||||
// TODO: add "Set precision as displayed" here once `formulas.calculationOptions` takes
|
||||
// `precisionAsDisplayed` (PR #1366); the model already stores it.
|
||||
}
|
||||
|
||||
/// Maximum Iterations and Maximum Change, live only while iterative calculation is on.
|
||||
fn iteration_options(app: &mut SheetApp, ui: &mut Ui, lang: Language, calc: &gridcraft_engine::model::CalcSettings, iterative: bool) {
|
||||
ui.add_enabled_ui(iterative, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
@@ -1070,10 +1089,6 @@ fn options(app: &mut SheetApp, ui: &mut Ui) {
|
||||
});
|
||||
});
|
||||
});
|
||||
// TODO: add "Set precision as displayed" here once `formulas.calculationOptions` takes
|
||||
// `precisionAsDisplayed` (PR #1366); the model already stores it.
|
||||
ui.add_space(6.0);
|
||||
dim(ui, lang.tr("Calculation settings are saved with the workbook."));
|
||||
}
|
||||
|
||||
/// Refuses a typed setting with a message, leaving the workbook as it was. The Enter that
|
||||
|
||||
@@ -78,6 +78,32 @@ pub fn highlight(text: &str, lang: Lang) -> Vec<(Range<usize>, Token)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// End (exclusive, in chars) of the Python string literal opening with quote `c` at char `i`.
|
||||
fn python_string_end(chars: &[(usize, char)], i: usize, c: char) -> usize {
|
||||
let triple = chars.get(i + 1).is_some_and(|(_, d)| *d == c) && chars.get(i + 2).is_some_and(|(_, d)| *d == c);
|
||||
let mut j = if triple { i + 3 } else { i + 1 };
|
||||
loop {
|
||||
match chars.get(j) {
|
||||
None => break,
|
||||
Some((_, '\\')) => j += 2,
|
||||
Some((_, '\n')) if !triple => break,
|
||||
Some((_, d)) if *d == c => {
|
||||
if !triple {
|
||||
j += 1;
|
||||
break;
|
||||
}
|
||||
if chars.get(j + 1).is_some_and(|(_, d)| *d == c) && chars.get(j + 2).is_some_and(|(_, d)| *d == c) {
|
||||
j += 3;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
Some(_) => j += 1,
|
||||
}
|
||||
}
|
||||
j.min(chars.len())
|
||||
}
|
||||
|
||||
fn python(text: &str) -> Vec<(Range<usize>, Token)> {
|
||||
let chars: Vec<(usize, char)> = text.char_indices().collect();
|
||||
let end_of = |i: usize| chars.get(i).map(|(b, _)| *b).unwrap_or(text.len());
|
||||
@@ -92,28 +118,7 @@ fn python(text: &str) -> Vec<(Range<usize>, Token)> {
|
||||
out.push((start..end_of(j), Token::Comment));
|
||||
i = j;
|
||||
} else if c == '"' || c == '\'' {
|
||||
let triple = chars.get(i + 1).is_some_and(|(_, d)| *d == c) && chars.get(i + 2).is_some_and(|(_, d)| *d == c);
|
||||
let mut j = if triple { i + 3 } else { i + 1 };
|
||||
loop {
|
||||
match chars.get(j) {
|
||||
None => break,
|
||||
Some((_, '\\')) => j += 2,
|
||||
Some((_, '\n')) if !triple => break,
|
||||
Some((_, d)) if *d == c => {
|
||||
if !triple {
|
||||
j += 1;
|
||||
break;
|
||||
}
|
||||
if chars.get(j + 1).is_some_and(|(_, d)| *d == c) && chars.get(j + 2).is_some_and(|(_, d)| *d == c) {
|
||||
j += 3;
|
||||
break;
|
||||
}
|
||||
j += 1;
|
||||
}
|
||||
Some(_) => j += 1,
|
||||
}
|
||||
}
|
||||
let j = j.min(chars.len());
|
||||
let j = python_string_end(&chars, i, c);
|
||||
out.push((start..end_of(j), Token::String));
|
||||
i = j;
|
||||
} else if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(|(_, d)| d.is_ascii_digit())) {
|
||||
|
||||
+52
-32
@@ -2046,38 +2046,46 @@ fn update_cursor(app: &mut SheetApp, ui: &egui::Ui, resp: &egui::Response, geo:
|
||||
}
|
||||
// A held drag owns the cursor even outside the grid; a press below egui's threshold still
|
||||
// has Drag::None and uses hover. A click can briefly leave a drag state after release.
|
||||
if resp.dragged() && pointer.primary_down() {
|
||||
let cursor = match app.grid.drag {
|
||||
Drag::None => None,
|
||||
Drag::Select => Some(CursorIcon::Cell),
|
||||
Drag::Rows(_) | Drag::Cols(_) => Some(CursorIcon::Default),
|
||||
Drag::Fill { .. } => Some(CursorIcon::Crosshair),
|
||||
Drag::Move { .. } | Drag::MoveCols { .. } | Drag::Object { resize: false, .. } => Some(CursorIcon::Move),
|
||||
Drag::ResizeCol { .. } => Some(CursorIcon::ResizeColumn),
|
||||
Drag::ResizeRow { .. } => Some(CursorIcon::ResizeRow),
|
||||
Drag::Freeze { cols, .. } => Some(if cols { CursorIcon::ResizeHorizontal } else { CursorIcon::ResizeVertical }),
|
||||
Drag::Object { resize: true, .. } => Some(CursorIcon::ResizeNwSe),
|
||||
};
|
||||
if let Some(cursor) = cursor {
|
||||
ctx.set_cursor_icon(cursor);
|
||||
return;
|
||||
}
|
||||
if resp.dragged()
|
||||
&& pointer.primary_down()
|
||||
&& let Some(cursor) = drag_cursor(&app.grid.drag)
|
||||
{
|
||||
ctx.set_cursor_icon(cursor);
|
||||
return;
|
||||
}
|
||||
if !resp.hovered() {
|
||||
return;
|
||||
}
|
||||
let Some(p) = pos else { return };
|
||||
hover_cursor(app, ctx, resp, &pointer, geo, sh, p);
|
||||
}
|
||||
|
||||
/// The cursor a held drag shows, or `None` when the drag leaves it to hover.
|
||||
fn drag_cursor(drag: &Drag) -> Option<CursorIcon> {
|
||||
match drag {
|
||||
Drag::None => None,
|
||||
Drag::Select => Some(CursorIcon::Cell),
|
||||
Drag::Rows(_) | Drag::Cols(_) => Some(CursorIcon::Default),
|
||||
Drag::Fill { .. } => Some(CursorIcon::Crosshair),
|
||||
Drag::Move { .. } | Drag::MoveCols { .. } | Drag::Object { resize: false, .. } => Some(CursorIcon::Move),
|
||||
Drag::ResizeCol { .. } => Some(CursorIcon::ResizeColumn),
|
||||
Drag::ResizeRow { .. } => Some(CursorIcon::ResizeRow),
|
||||
Drag::Freeze { cols, .. } => Some(if *cols { CursorIcon::ResizeHorizontal } else { CursorIcon::ResizeVertical }),
|
||||
Drag::Object { resize: true, .. } => Some(CursorIcon::ResizeNwSe),
|
||||
}
|
||||
}
|
||||
|
||||
/// The cursor over the grid when no drag owns it: freeze bars, header edges, the fill handle, cells.
|
||||
fn hover_cursor(app: &SheetApp, ctx: &egui::Context, resp: &egui::Response, pointer: &egui::PointerState, geo: &Geo, sh: &Sheet, p: Pos2) {
|
||||
let (freeze_cols, freeze_rows) = freeze_handles(geo);
|
||||
if freeze_cols.contains(p) || freeze_rows.contains(p) {
|
||||
let cols = freeze_cols.contains(p);
|
||||
ctx.set_cursor_icon(if cols { CursorIcon::ResizeHorizontal } else { CursorIcon::ResizeVertical });
|
||||
if app.grid.drag == Drag::None && !pointer.any_down() {
|
||||
let tip = if cols { "Drag to freeze columns" } else { "Drag to freeze rows" };
|
||||
resp.clone().on_hover_text_at_pointer(crate::i18n::current(ctx).tr(tip));
|
||||
}
|
||||
freeze_cursor(app, ctx, resp, pointer, freeze_cols.contains(p));
|
||||
return;
|
||||
}
|
||||
let in_cells = geo.in_cells(p);
|
||||
let in_col_header = p.y < geo.cells.top() && p.y >= geo.head_top() && p.x >= geo.cells.left();
|
||||
let in_row_header = p.x < geo.cells.left() && p.x >= geo.head_left() && p.y >= geo.cells.top();
|
||||
let near_col_edge = in_col_header && col_edge(geo, sh, p.x).is_some();
|
||||
let sel = app.session.active().map(|d| d.selection.clone()).unwrap_or_default();
|
||||
let cur_rect = geo.range_rect(sh, sel.current());
|
||||
let handle = Rect::from_center_size(cur_rect.right_bottom(), vec2(9.0, 9.0));
|
||||
@@ -2089,16 +2097,8 @@ fn update_cursor(app: &mut SheetApp, ui: &egui::Ui, resp: &egui::Response, geo:
|
||||
let near_corner = selected_object.is_some_and(|(_, _, rect)| (p - rect.right_bottom()).length() < OBJECT_RESIZE_RADIUS);
|
||||
if app.grid.drag == Drag::None && near_corner {
|
||||
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||
} else if (in_col_header && col_edge(geo, sh, p.x).is_some()) || (in_row_header && row_edge(geo, sh, p.y).is_some()) {
|
||||
let near_col_edge = in_col_header && col_edge(geo, sh, p.x).is_some();
|
||||
// Just past a hidden column or row, Excel shows a split cursor: dragging or
|
||||
// double-clicking there unhides it.
|
||||
let hidden = app.grid.drag == Drag::None
|
||||
&& if near_col_edge { hidden_col_edge(geo, sh, p.x).is_some() } else { hidden_row_edge(geo, sh, p.y).is_some() };
|
||||
match pointer.hover_pos() {
|
||||
Some(at) if hidden => paint_split_cursor(ctx, at, near_col_edge),
|
||||
_ => ctx.set_cursor_icon(if near_col_edge { CursorIcon::ResizeColumn } else { CursorIcon::ResizeRow }),
|
||||
}
|
||||
} else if near_col_edge || (in_row_header && row_edge(geo, sh, p.y).is_some()) {
|
||||
header_edge_cursor(app, ctx, pointer, geo, sh, p, near_col_edge);
|
||||
} else if on_handle && app.editor.is_none() {
|
||||
ctx.set_cursor_icon(CursorIcon::Crosshair);
|
||||
if app.grid.drag == Drag::None && !pointer.any_down() {
|
||||
@@ -2115,6 +2115,26 @@ fn update_cursor(app: &mut SheetApp, ui: &egui::Ui, resp: &egui::Response, geo:
|
||||
}
|
||||
}
|
||||
|
||||
/// The resize cursor for dragging a freeze bar.
|
||||
fn freeze_cursor(app: &SheetApp, ctx: &egui::Context, resp: &egui::Response, pointer: &egui::PointerState, cols: bool) {
|
||||
ctx.set_cursor_icon(if cols { CursorIcon::ResizeHorizontal } else { CursorIcon::ResizeVertical });
|
||||
if app.grid.drag == Drag::None && !pointer.any_down() {
|
||||
let tip = if cols { "Drag to freeze columns" } else { "Drag to freeze rows" };
|
||||
resp.clone().on_hover_text_at_pointer(crate::i18n::current(ctx).tr(tip));
|
||||
}
|
||||
}
|
||||
|
||||
/// The resize cursor over a column or row header edge. Just past a hidden column or row, Excel
|
||||
/// shows a split cursor: dragging or double-clicking there unhides it.
|
||||
fn header_edge_cursor(app: &SheetApp, ctx: &egui::Context, pointer: &egui::PointerState, geo: &Geo, sh: &Sheet, p: Pos2, near_col_edge: bool) {
|
||||
let hidden =
|
||||
app.grid.drag == Drag::None && if near_col_edge { hidden_col_edge(geo, sh, p.x).is_some() } else { hidden_row_edge(geo, sh, p.y).is_some() };
|
||||
match pointer.hover_pos() {
|
||||
Some(at) if hidden => paint_split_cursor(ctx, at, near_col_edge),
|
||||
_ => ctx.set_cursor_icon(if near_col_edge { CursorIcon::ResizeColumn } else { CursorIcon::ResizeRow }),
|
||||
}
|
||||
}
|
||||
|
||||
fn interact(app: &mut SheetApp, ui: &mut egui::Ui, resp: &egui::Response, geo: &Geo, sh: &Sheet, wb: &Workbook) {
|
||||
// Text selection and editing must not also move the object or edit worksheet cells.
|
||||
if app.text_box_editor.is_some() || app.grid.hf_edit.is_some() {
|
||||
|
||||
+46
-50
@@ -359,59 +359,55 @@ pub fn read_styles(root: &El, checkboxes: &[bool], minor_font: &str) -> StylesIn
|
||||
}
|
||||
}
|
||||
}
|
||||
let dxfs = root
|
||||
.child("dxfs")
|
||||
.map(|d| {
|
||||
d.kids("dxf")
|
||||
.map(|x| {
|
||||
let mut s = Style::default();
|
||||
if let Some(e) = x.child("font") {
|
||||
let f = read_font(e, &palette);
|
||||
// Differential fonts rarely name a face; unless stated, keep the default face and size.
|
||||
let has_size = e.child("sz").and_then(|n| n.attr_f64("val")).is_some();
|
||||
let has_name = e.child("name").and_then(|n| n.attr("val")).is_some();
|
||||
let name = if has_name { f.name.clone() } else { s.font.name.clone() };
|
||||
let size = if has_size { f.size } else { s.font.size };
|
||||
let def = Font::default();
|
||||
let st = &mut s.dxf_stated;
|
||||
st.bold = e.child("b").is_some() && !f.bold;
|
||||
st.italic = e.child("i").is_some() && !f.italic;
|
||||
st.strike = e.child("strike").is_some() && !f.strike;
|
||||
st.underline = e.child("u").is_some() && f.underline == Underline::None;
|
||||
st.size = has_size && size == def.size;
|
||||
st.name = has_name && name == def.name;
|
||||
st.scheme = e.child_val("scheme").map(str::to_string);
|
||||
// A differential font's scheme is in `dxf_stated`.
|
||||
s.font = Font { name, size, minor: s.font.minor, ..f };
|
||||
}
|
||||
if let Some(n) = x.child("numFmt") {
|
||||
if let Some(code) = n.attr("formatCode") {
|
||||
s.num_fmt = NumFmt::new(code);
|
||||
} else if let Some(id) = n.attr_u32("numFmtId") {
|
||||
s.num_fmt = num_fmt_code(id, &fmts);
|
||||
}
|
||||
}
|
||||
if let Some(f) = x.child("fill") {
|
||||
s.fill = read_fill(f, &palette, true);
|
||||
}
|
||||
if let Some(b) = x.child("border") {
|
||||
s.borders = read_border(b, &palette);
|
||||
}
|
||||
if let Some(a) = x.child("alignment") {
|
||||
s.align = read_alignment(a);
|
||||
}
|
||||
if let Some(p) = x.child("protection") {
|
||||
s.protection = read_protection(p);
|
||||
s.dxf_stated.protection = s.protection == Protection::default();
|
||||
}
|
||||
s
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let dxfs = root.child("dxfs").map(|d| d.kids("dxf").map(|x| read_dxf(x, &palette, &fmts)).collect()).unwrap_or_default();
|
||||
StylesIn { xfs, dxfs, palette, named, num_fmts: fmts }
|
||||
}
|
||||
|
||||
/// One differential format (`dxf`): the style it sets over a cell's own.
|
||||
fn read_dxf(x: &El, palette: &[u32], fmts: &HashMap<u32, String>) -> Style {
|
||||
let mut s = Style::default();
|
||||
if let Some(e) = x.child("font") {
|
||||
let f = read_font(e, palette);
|
||||
// Differential fonts rarely name a face; unless stated, keep the default face and size.
|
||||
let has_size = e.child("sz").and_then(|n| n.attr_f64("val")).is_some();
|
||||
let has_name = e.child("name").and_then(|n| n.attr("val")).is_some();
|
||||
let name = if has_name { f.name.clone() } else { s.font.name.clone() };
|
||||
let size = if has_size { f.size } else { s.font.size };
|
||||
let def = Font::default();
|
||||
let st = &mut s.dxf_stated;
|
||||
st.bold = e.child("b").is_some() && !f.bold;
|
||||
st.italic = e.child("i").is_some() && !f.italic;
|
||||
st.strike = e.child("strike").is_some() && !f.strike;
|
||||
st.underline = e.child("u").is_some() && f.underline == Underline::None;
|
||||
st.size = has_size && size == def.size;
|
||||
st.name = has_name && name == def.name;
|
||||
st.scheme = e.child_val("scheme").map(str::to_string);
|
||||
// A differential font's scheme is in `dxf_stated`.
|
||||
s.font = Font { name, size, minor: s.font.minor, ..f };
|
||||
}
|
||||
if let Some(n) = x.child("numFmt") {
|
||||
if let Some(code) = n.attr("formatCode") {
|
||||
s.num_fmt = NumFmt::new(code);
|
||||
} else if let Some(id) = n.attr_u32("numFmtId") {
|
||||
s.num_fmt = num_fmt_code(id, fmts);
|
||||
}
|
||||
}
|
||||
if let Some(f) = x.child("fill") {
|
||||
s.fill = read_fill(f, palette, true);
|
||||
}
|
||||
if let Some(b) = x.child("border") {
|
||||
s.borders = read_border(b, palette);
|
||||
}
|
||||
if let Some(a) = x.child("alignment") {
|
||||
s.align = read_alignment(a);
|
||||
}
|
||||
if let Some(p) = x.child("protection") {
|
||||
s.protection = read_protection(p);
|
||||
s.dxf_stated.protection = s.protection == Protection::default();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- writing
|
||||
|
||||
pub fn color_attrs(c: &Color) -> String {
|
||||
|
||||
+50
-38
@@ -116,6 +116,55 @@ pub fn catalog_keys(source: &str) -> BTreeSet<String> {
|
||||
keys
|
||||
}
|
||||
|
||||
/// The character a `\u{...}` escape stands for, if it names a valid code point.
|
||||
fn unicode_escape(chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>) -> Option<char> {
|
||||
let mut hex = String::new();
|
||||
for (_, c) in chars.by_ref() {
|
||||
match c {
|
||||
'}' => break,
|
||||
'{' => {}
|
||||
c => hex.push(c),
|
||||
}
|
||||
}
|
||||
u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
|
||||
}
|
||||
|
||||
/// The value of a string literal whose opening quote was just read, and the byte offset past
|
||||
/// its closing quote (`len`, the source's length, when it is never closed).
|
||||
fn string_literal(chars: &mut std::iter::Peekable<std::str::CharIndices<'_>>, len: usize) -> (String, usize) {
|
||||
let mut value = String::new();
|
||||
let mut end = len;
|
||||
while let Some((j, c)) = chars.next() {
|
||||
match c {
|
||||
'"' => {
|
||||
end = j + 1;
|
||||
break;
|
||||
}
|
||||
'\\' => match chars.next().map(|(_, c)| c) {
|
||||
Some('n') => value.push('\n'),
|
||||
Some('t') => value.push('\t'),
|
||||
Some('r') => value.push('\r'),
|
||||
Some('0') => value.push('\0'),
|
||||
Some(c @ ('\\' | '"' | '\'')) => value.push(c),
|
||||
Some('u') => {
|
||||
if let Some(c) = unicode_escape(chars) {
|
||||
value.push(c);
|
||||
}
|
||||
}
|
||||
// A line continuation skips the newline and the next line's indentation.
|
||||
Some('\n') => {
|
||||
while chars.peek().is_some_and(|(_, c)| c.is_whitespace()) {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
c => value.push(c),
|
||||
}
|
||||
}
|
||||
(value, end)
|
||||
}
|
||||
|
||||
/// Every string literal outside `//` comments: its decoded value, and the byte range from its
|
||||
/// opening quote to just past its closing one.
|
||||
fn literals(source: &str) -> Vec<(String, usize, usize)> {
|
||||
@@ -133,44 +182,7 @@ fn literals(source: &str) -> Vec<(String, usize, usize)> {
|
||||
if c != '"' {
|
||||
continue;
|
||||
}
|
||||
let mut value = String::new();
|
||||
let mut end = source.len();
|
||||
while let Some((j, c)) = chars.next() {
|
||||
match c {
|
||||
'"' => {
|
||||
end = j + 1;
|
||||
break;
|
||||
}
|
||||
'\\' => match chars.next().map(|(_, c)| c) {
|
||||
Some('n') => value.push('\n'),
|
||||
Some('t') => value.push('\t'),
|
||||
Some('r') => value.push('\r'),
|
||||
Some('0') => value.push('\0'),
|
||||
Some(c @ ('\\' | '"' | '\'')) => value.push(c),
|
||||
Some('u') => {
|
||||
let mut hex = String::new();
|
||||
for (_, c) in chars.by_ref() {
|
||||
match c {
|
||||
'}' => break,
|
||||
'{' => {}
|
||||
c => hex.push(c),
|
||||
}
|
||||
}
|
||||
if let Some(c) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
|
||||
value.push(c);
|
||||
}
|
||||
}
|
||||
// A line continuation skips the newline and the next line's indentation.
|
||||
Some('\n') => {
|
||||
while chars.peek().is_some_and(|(_, c)| c.is_whitespace()) {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
c => value.push(c),
|
||||
}
|
||||
}
|
||||
let (value, end) = string_literal(&mut chars, source.len());
|
||||
out.push((value, i, end));
|
||||
}
|
||||
out
|
||||
|
||||
Reference in New Issue
Block a user