mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-26 03:36:54 -06:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61f36a78c8 |
@@ -1454,6 +1454,34 @@ fn show_levels(s: &mut Session, p: &Json) -> Result<Json> {
|
|||||||
|
|
||||||
/// Data ▸ Outline ▸ Settings: whether summary rows sit below their detail and summary columns
|
/// Data ▸ Outline ▸ Settings: whether summary rows sit below their detail and summary columns
|
||||||
/// right of it. A changed direction moves each collapsed group's mark (+) to its new summary line.
|
/// right of it. A changed direction moves each collapsed group's mark (+) to its new summary line.
|
||||||
|
/// Moves the collapsed marks of a line axis to the summary line on its new side (`on` is the new
|
||||||
|
/// setting, `was_before` the old one). A line that nested groups share keeps its mark on the outermost
|
||||||
|
/// group whose detail is all hidden, or the outermost when none is.
|
||||||
|
fn move_marks(map: &mut std::collections::BTreeMap<u32, LineInfo>, was_before: bool, on: bool) {
|
||||||
|
let mut marked: Vec<outline::OutlineGroup> =
|
||||||
|
outline::groups_with(map, was_before).into_iter().filter(|g| g.has_summary() && map.get(&g.summary()).is_some_and(|e| e.collapsed)).collect();
|
||||||
|
// A mark on a line that nested groups share moves with the outermost of them whose
|
||||||
|
// detail is all hidden, or with the outermost when none is.
|
||||||
|
let mut owner: std::collections::BTreeMap<u32, (bool, u8)> = std::collections::BTreeMap::new();
|
||||||
|
for g in &marked {
|
||||||
|
let key = (!g.collapsed, g.level);
|
||||||
|
let best = owner.entry(g.summary()).or_insert(key);
|
||||||
|
*best = (*best).min(key);
|
||||||
|
}
|
||||||
|
marked.retain(|g| owner.get(&g.summary()) == Some(&(!g.collapsed, g.level)));
|
||||||
|
for g in &marked {
|
||||||
|
if let Some(e) = map.get_mut(&g.summary()) {
|
||||||
|
e.collapsed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for g in marked {
|
||||||
|
let g = outline::OutlineGroup { summary_before: !on, ..g };
|
||||||
|
if g.has_summary() {
|
||||||
|
map.entry(g.summary()).or_default().collapsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn outline_settings(s: &mut Session, p: &Json) -> Result<Json> {
|
fn outline_settings(s: &mut Session, p: &Json) -> Result<Json> {
|
||||||
let si = target_sheet(s, p)?;
|
let si = target_sheet(s, p)?;
|
||||||
edit(s, |cx| {
|
edit(s, |cx| {
|
||||||
@@ -1469,31 +1497,7 @@ fn outline_settings(s: &mut Session, p: &Json) -> Result<Json> {
|
|||||||
if was_before != on {
|
if was_before != on {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let map = if rows { &mut sh.rows } else { &mut sh.cols };
|
move_marks(if rows { &mut sh.rows } else { &mut sh.cols }, was_before, on);
|
||||||
let mut marked: Vec<outline::OutlineGroup> = outline::groups_with(map, was_before)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|g| g.has_summary() && map.get(&g.summary()).is_some_and(|e| e.collapsed))
|
|
||||||
.collect();
|
|
||||||
// A mark on a line that nested groups share moves with the outermost of them whose
|
|
||||||
// detail is all hidden, or with the outermost when none is.
|
|
||||||
let mut owner: std::collections::BTreeMap<u32, (bool, u8)> = std::collections::BTreeMap::new();
|
|
||||||
for g in &marked {
|
|
||||||
let key = (!g.collapsed, g.level);
|
|
||||||
let best = owner.entry(g.summary()).or_insert(key);
|
|
||||||
*best = (*best).min(key);
|
|
||||||
}
|
|
||||||
marked.retain(|g| owner.get(&g.summary()) == Some(&(!g.collapsed, g.level)));
|
|
||||||
for g in &marked {
|
|
||||||
if let Some(e) = map.get_mut(&g.summary()) {
|
|
||||||
e.collapsed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for g in marked {
|
|
||||||
let g = outline::OutlineGroup { summary_before: !on, ..g };
|
|
||||||
if g.has_summary() {
|
|
||||||
map.entry(g.summary()).or_default().collapsed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(json!({"summaryBelow": sh.summary_below, "summaryRight": sh.summary_right}))
|
Ok(json!({"summaryBelow": sh.summary_below, "summaryRight": sh.summary_right}))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -96,73 +96,75 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a cell on the active sheet is empty.
|
fn auto_sum(s: &mut Session, p: &Json) -> Result<Json> {
|
||||||
fn blank(sh: &gridcraft_model::sheet::Sheet, row: u32, col: u32) -> bool {
|
let func = str_param(p, "function").unwrap_or("SUM").to_ascii_uppercase();
|
||||||
sh.value(CellRef::new(row, col)).is_empty()
|
let d = s.doc()?;
|
||||||
}
|
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
|
||||||
|
let sel = match str_param(p, "range") {
|
||||||
fn sum_formula(func: &str, from: CellRef, to: CellRef) -> String {
|
Some(_) => target_range(s, p)?,
|
||||||
format!("={func}({})", RangeRef::new(from, to).a1())
|
None => d.selection.current(),
|
||||||
}
|
};
|
||||||
|
if !sel.is_single() {
|
||||||
/// Totals for a multi-cell selection as `(cell, formula)` writes. `None` when the sheet has no data.
|
// A selection: totals go in its empty last row or column, as Excel's do, and otherwise
|
||||||
fn selection_totals(sh: &gridcraft_model::sheet::Sheet, sel: RangeRef, func: &str) -> Result<Option<Vec<(CellRef, String)>>> {
|
// past it: to the right of a single row, below each column of anything taller. Columns
|
||||||
// A selection: totals go in its empty last row or column, as Excel's do, and otherwise
|
// past the sheet's data get no total (a whole-row selection doesn't write 16,384
|
||||||
// past it: to the right of a single row, below each column of anything taller. Columns
|
// formulas), nor do rows past it, and all totals are one edit (one undo step).
|
||||||
// past the sheet's data get no total (a whole-row selection doesn't write 16,384
|
let Some(used) = sh.cells.used_range() else { return ok() };
|
||||||
// formulas), nor do rows past it, and all totals are one edit (one undo step).
|
let empty = |row: u32, col: u32| sh.value(CellRef::new(row, col)).is_empty();
|
||||||
let Some(used) = sh.cells.used_range() else { return Ok(None) };
|
let sum = |from: CellRef, to: CellRef| format!("={func}({})", RangeRef::new(from, to).a1());
|
||||||
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) }
|
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.
|
// 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 row = sel.start.row;
|
let end_col = if sel.is_full_rows() { last_col } else { sel.end.col };
|
||||||
let last_col = used.end.col.min(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) };
|
||||||
let end_col = if sel.is_full_rows() { last_col } else { sel.end.col };
|
if out >= gridcraft_core::MAX_COLS {
|
||||||
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) };
|
return Err(EngineError::Other("There's no room to the right of the selection for the total.".into()));
|
||||||
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();
|
||||||
}
|
}
|
||||||
Ok(vec![(CellRef::new(row, out), sum_formula(func, CellRef::new(row, sel.start.col), CellRef::new(row, data_end)))])
|
// Single cell: guess the range above, else to the left.
|
||||||
}
|
let at = sel.start;
|
||||||
|
|
||||||
/// 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 numeric = |c: CellRef| sh.value(c).is_number();
|
||||||
let mut range = None;
|
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))) {
|
if at.row > 0 && (numeric(CellRef::new(at.row - 1, at.col)) || at.col == 0 || !numeric(CellRef::new(at.row, at.col - 1))) {
|
||||||
@@ -184,33 +186,7 @@ fn guessed_range(sh: &gridcraft_model::sheet::Sheet, at: CellRef) -> Option<Rang
|
|||||||
}
|
}
|
||||||
range = Some(RangeRef::new(CellRef::new(at.row, left), CellRef::new(at.row, at.col - 1)));
|
range = Some(RangeRef::new(CellRef::new(at.row, left), CellRef::new(at.row, at.col - 1)));
|
||||||
}
|
}
|
||||||
range
|
let text = match 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()),
|
Some(r) => format!("={func}({})", r.a1()),
|
||||||
None => {
|
None => {
|
||||||
s.ui_requests.push(crate::UiRequest::EditCell(Some(format!("={func}()"))));
|
s.ui_requests.push(crate::UiRequest::EditCell(Some(format!("={func}()"))));
|
||||||
|
|||||||
+32
-52
@@ -2046,46 +2046,38 @@ 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
|
// 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.
|
// has Drag::None and uses hover. A click can briefly leave a drag state after release.
|
||||||
if resp.dragged()
|
if resp.dragged() && pointer.primary_down() {
|
||||||
&& pointer.primary_down()
|
let cursor = match app.grid.drag {
|
||||||
&& let Some(cursor) = drag_cursor(&app.grid.drag)
|
Drag::None => None,
|
||||||
{
|
Drag::Select => Some(CursorIcon::Cell),
|
||||||
ctx.set_cursor_icon(cursor);
|
Drag::Rows(_) | Drag::Cols(_) => Some(CursorIcon::Default),
|
||||||
return;
|
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.hovered() {
|
if !resp.hovered() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(p) = pos else { 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) {
|
if freeze_cols.contains(p) || freeze_rows.contains(p) {
|
||||||
freeze_cursor(app, ctx, resp, pointer, freeze_cols.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));
|
||||||
|
}
|
||||||
return;
|
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_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 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 sel = app.session.active().map(|d| d.selection.clone()).unwrap_or_default();
|
||||||
let cur_rect = geo.range_rect(sh, sel.current());
|
let cur_rect = geo.range_rect(sh, sel.current());
|
||||||
let handle = Rect::from_center_size(cur_rect.right_bottom(), vec2(9.0, 9.0));
|
let handle = Rect::from_center_size(cur_rect.right_bottom(), vec2(9.0, 9.0));
|
||||||
@@ -2097,8 +2089,16 @@ fn hover_cursor(app: &SheetApp, ctx: &egui::Context, resp: &egui::Response, poin
|
|||||||
let near_corner = selected_object.is_some_and(|(_, _, rect)| (p - rect.right_bottom()).length() < OBJECT_RESIZE_RADIUS);
|
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 {
|
if app.grid.drag == Drag::None && near_corner {
|
||||||
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
|
ctx.set_cursor_icon(CursorIcon::ResizeNwSe);
|
||||||
} else if near_col_edge || (in_row_header && row_edge(geo, sh, p.y).is_some()) {
|
} else if (in_col_header && col_edge(geo, sh, p.x).is_some()) || (in_row_header && row_edge(geo, sh, p.y).is_some()) {
|
||||||
header_edge_cursor(app, ctx, pointer, geo, sh, p, near_col_edge);
|
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 on_handle && app.editor.is_none() {
|
} else if on_handle && app.editor.is_none() {
|
||||||
ctx.set_cursor_icon(CursorIcon::Crosshair);
|
ctx.set_cursor_icon(CursorIcon::Crosshair);
|
||||||
if app.grid.drag == Drag::None && !pointer.any_down() {
|
if app.grid.drag == Drag::None && !pointer.any_down() {
|
||||||
@@ -2115,26 +2115,6 @@ fn hover_cursor(app: &SheetApp, ctx: &egui::Context, resp: &egui::Response, poin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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) {
|
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.
|
// 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() {
|
if app.text_box_editor.is_some() || app.grid.hf_edit.is_some() {
|
||||||
|
|||||||
@@ -165,6 +165,35 @@ impl Dir<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The band's buttons and brackets for this frame.
|
/// The band's buttons and brackets for this frame.
|
||||||
|
/// The bracket for one group's detail, and its tick, on the lane `across`; `button` is the group's
|
||||||
|
/// summary button when it is on screen.
|
||||||
|
fn group_bracket(out: &mut Layout, dir: &Dir, grp: &OutlineGroup, button: Option<f32>, across: f32, side: f32) {
|
||||||
|
let first = dir.first_visible(grp.start, grp.end);
|
||||||
|
let Some((first_line, (a0, _))) = first else { return };
|
||||||
|
if grp.summary_before {
|
||||||
|
// From the button down (or across) to the last detail line, with the tick there.
|
||||||
|
let a0 = button.map_or(a0, |mid| mid + side / 2.0);
|
||||||
|
let end = dir.start(grp.end) + dir.size(grp.end);
|
||||||
|
let a1 = end.min(dir.hi);
|
||||||
|
if a1 <= a0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tick = (end <= dir.hi).then(|| [dir.point(a1 - 0.5, across), dir.point(a1 - 0.5, across + side / 2.0)]);
|
||||||
|
out.brackets.push(Bracket { line: [dir.point(a0, across), dir.point(a1 - 0.5, across)], tick });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let a1 = match button {
|
||||||
|
Some(mid) => mid - side / 2.0,
|
||||||
|
None => dir.start(grp.summary()).min(dir.hi),
|
||||||
|
};
|
||||||
|
if a1 <= a0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let start_shown = first_line == grp.start && (a0 - dir.start(grp.start)).abs() < 0.5;
|
||||||
|
let tick = start_shown.then(|| [dir.point(a0 + 0.5, across), dir.point(a0 + 0.5, across + side / 2.0)]);
|
||||||
|
out.brackets.push(Bracket { line: [dir.point(a0 + 0.5, across), dir.point(a1, across)], tick });
|
||||||
|
}
|
||||||
|
|
||||||
pub fn layout(geo: &Geo, sh: &Sheet) -> Layout {
|
pub fn layout(geo: &Geo, sh: &Sheet) -> Layout {
|
||||||
let mut out = Layout::default();
|
let mut out = Layout::default();
|
||||||
let zoom = (geo.z / DISPLAY_SCALE).max(0.1);
|
let zoom = (geo.z / DISPLAY_SCALE).max(0.1);
|
||||||
@@ -212,30 +241,7 @@ pub fn layout(geo: &Geo, sh: &Sheet) -> Layout {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// The bracket, over the part of the detail that is on screen.
|
// The bracket, over the part of the detail that is on screen.
|
||||||
let first = dir.first_visible(grp.start, grp.end);
|
group_bracket(&mut out, dir, grp, button, across, side);
|
||||||
let Some((first_line, (a0, _))) = first else { continue };
|
|
||||||
if grp.summary_before {
|
|
||||||
// From the button down (or across) to the last detail line, with the tick there.
|
|
||||||
let a0 = button.map_or(a0, |mid| mid + side / 2.0);
|
|
||||||
let end = dir.start(grp.end) + dir.size(grp.end);
|
|
||||||
let a1 = end.min(dir.hi);
|
|
||||||
if a1 <= a0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let tick = (end <= dir.hi).then(|| [dir.point(a1 - 0.5, across), dir.point(a1 - 0.5, across + side / 2.0)]);
|
|
||||||
out.brackets.push(Bracket { line: [dir.point(a0, across), dir.point(a1 - 0.5, across)], tick });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let a1 = match button {
|
|
||||||
Some(mid) => mid - side / 2.0,
|
|
||||||
None => dir.start(grp.summary()).min(dir.hi),
|
|
||||||
};
|
|
||||||
if a1 <= a0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let start_shown = first_line == grp.start && (a0 - dir.start(grp.start)).abs() < 0.5;
|
|
||||||
let tick = start_shown.then(|| [dir.point(a0 + 0.5, across), dir.point(a0 + 0.5, across + side / 2.0)]);
|
|
||||||
out.brackets.push(Bracket { line: [dir.point(a0 + 0.5, across), dir.point(a1, across)], tick });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|||||||
+50
-46
@@ -359,55 +359,59 @@ pub fn read_styles(root: &El, checkboxes: &[bool], minor_font: &str) -> StylesIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dxfs = root.child("dxfs").map(|d| d.kids("dxf").map(|x| read_dxf(x, &palette, &fmts)).collect()).unwrap_or_default();
|
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();
|
||||||
StylesIn { xfs, dxfs, palette, named, num_fmts: fmts }
|
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
|
// ---------------------------------------------------------------- writing
|
||||||
|
|
||||||
pub fn color_attrs(c: &Color) -> String {
|
pub fn color_attrs(c: &Color) -> String {
|
||||||
|
|||||||
Reference in New Issue
Block a user