//! Formatting: Home › Font, Alignment, Number, Styles, Cells › Format. use gridcraft_core::{CellRef, RangeRef}; use gridcraft_model::*; use serde_json::{Value as Json, json}; use super::*; pub fn specs() -> Vec { vec![ // Font cmd!("home.bold", "Bold", ["Home", "Font"], Some("Cmd+B"), "{range?, on?: bool}", has_doc, |s, p| toggle( s, p, |st| st.font.bold, |st, v| st.font.bold = v )), cmd!("home.italic", "Italic", ["Home", "Font"], Some("Cmd+I"), "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.font.italic, |st, v| st.font.italic = v )), cmd!( "home.underline", "Underline", ["Home", "Font"], Some("Cmd+U"), "{range?, on?, style?: single|double|singleAccounting|doubleAccounting}", has_doc, underline ), cmd!("home.doubleUnderline", "Double Underline", ["Home", "Font"], None, "{range?}", has_doc, |s, p| underline( s, &with(p, "style", json!("double")) )), cmd!("home.strikethrough", "Strikethrough", ["Home", "Font"], Some("Cmd+Shift+X"), "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.font.strike, |st, v| st.font.strike = v )), cmd!("home.superscript", "Superscript", ["Home", "Font"], None, "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.font.vert == VertAlign::Superscript, |st, v| st.font.vert = if v { VertAlign::Superscript } else { VertAlign::Baseline } )), cmd!("home.subscript", "Subscript", ["Home", "Font"], None, "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.font.vert == VertAlign::Subscript, |st, v| st.font.vert = if v { VertAlign::Subscript } else { VertAlign::Baseline } )), cmd!("home.fontName", "Font", ["Home", "Font"], None, "{range?, name: \"Calibri\"}", has_doc, font_name), cmd!("home.fontSize", "Font Size", ["Home", "Font"], None, "{range?, size: 11}", has_doc, font_size), cmd!("home.increaseFontSize", "Increase Font Size", ["Home", "Font"], Some("Cmd+Shift+>"), "{range?}", has_doc, |s, p| step_font(s, p, true)), cmd!("home.decreaseFontSize", "Decrease Font Size", ["Home", "Font"], Some("Cmd+Shift+<"), "{range?}", has_doc, |s, p| step_font( s, p, false )), cmd!( "home.fontColor", "Font Color", ["Home", "Font"], None, "{range?, color: \"#C00000\" | \"auto\" | {theme: 4, tint: -0.25}}", has_doc, font_color ), cmd!("home.fillColor", "Fill Color", ["Home", "Font"], None, "{range?, color: \"#FFFF00\" | \"none\" | {theme, tint}}", has_doc, fill_color), cmd!( "home.borders", "Borders", ["Home", "Font"], None, "{range?, preset: bottom|top|left|right|none|all|outside|thickOutside|thickBottom|doubleBottom|topBottom|topThickBottom|topDoubleBottom|insideHorizontal|insideVertical|inside|diagonalDown|diagonalUp, style?: thin|medium|thick|dashed|dotted|double|hair…, color?}", has_doc, borders ), cmd!( "home.formatCells", "Format Cells…", ["Home", "Cells", "Format"], Some("Cmd+1"), "{range?, style: {font?, fill?, borders?, align?, numFmt?, protection?}} (partial Style JSON merged in)", has_doc, format_cells ), // Alignment cmd!("home.alignLeft", "Align Left", ["Home", "Alignment"], Some("Cmd+L"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Left)), cmd!("home.alignCenter", "Center", ["Home", "Alignment"], Some("Cmd+E"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Center)), cmd!("home.alignRight", "Align Right", ["Home", "Alignment"], Some("Cmd+R"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Right)), cmd!("home.justify", "Justify", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Justify)), cmd!("home.alignTop", "Top Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Top)), cmd!("home.alignMiddle", "Middle Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Center)), cmd!("home.alignBottom", "Bottom Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Bottom)), cmd!("home.wrapText", "Wrap Text", ["Home", "Alignment"], None, "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.align.wrap, |st, v| st.align.wrap = v )), cmd!("home.shrinkToFit", "Shrink to Fit", [], None, "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.align.shrink, |st, v| st.align.shrink = v )), cmd!("home.increaseIndent", "Increase Indent", ["Home", "Alignment"], Some("Ctrl+Alt+Tab"), "{range?}", has_doc, |s, p| indent(s, p, 1)), cmd!("home.decreaseIndent", "Decrease Indent", ["Home", "Alignment"], Some("Ctrl+Alt+Shift+Tab"), "{range?}", has_doc, |s, p| indent( s, p, -1 )), cmd!( "home.orientation", "Orientation", ["Home", "Alignment"], None, "{range?, angle: -90..90 | \"vertical\" | \"counterclockwise\" | \"clockwise\" | \"up\" | \"down\" | 0}", has_doc, orientation ), cmd!("home.mergeCenter", "Merge & Center", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| merge(s, p, "center")), cmd!("home.mergeAcross", "Merge Across", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| merge(s, p, "across")), cmd!("home.mergeCells", "Merge Cells", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| merge(s, p, "cells")), cmd!("home.unmergeCells", "Unmerge Cells", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| merge(s, p, "unmerge")), // Number cmd!( "home.numberFormat", "Number Format", ["Home", "Number"], None, "{range?, format: \"General\"|\"Number\"|\"Currency\"|\"Accounting\"|\"Short Date\"|\"Long Date\"|\"Time\"|\"Percentage\"|\"Fraction\"|\"Scientific\"|\"Text\" | code: \"0.00\"}", has_doc, number_format ), cmd!("home.accounting", "Accounting Number Format", ["Home", "Number"], None, "{range?, symbol?: \"$\"}", has_doc, |s, p| set_fmt( s, p, ACCOUNTING )), cmd!("home.percent", "Percent Style", ["Home", "Number"], Some("Ctrl+Shift+%"), "{range?}", has_doc, |s, p| set_fmt(s, p, "0%")), cmd!("home.comma", "Comma Style", ["Home", "Number"], None, "{range?}", has_doc, |s, p| set_fmt(s, p, COMMA)), cmd!("home.increaseDecimal", "Increase Decimal", ["Home", "Number"], None, "{range?}", has_doc, |s, p| decimals(s, p, 1)), cmd!("home.decreaseDecimal", "Decrease Decimal", ["Home", "Number"], None, "{range?}", has_doc, |s, p| decimals(s, p, -1)), // Styles cmd!( "home.cellStyle", "Cell Styles", ["Home", "Styles"], None, "{range?, name: \"Good\"|\"Bad\"|\"Neutral\"|\"Heading 1\"|\"Title\"|\"Total\"|\"Accent1\"…|\"Normal\"}", has_doc, cell_style ), // Cells › Format cmd!( "home.rowHeight", "Row Height…", ["Home", "Cells", "Format"], None, "{rows?: \"2:5\", height: 20 (points on screen)}", has_doc, row_height ), cmd!( "home.columnWidth", "Column Width…", ["Home", "Cells", "Format"], None, "{cols?: \"B:D\", width: 64 (points) | chars: 8.43}", has_doc, col_width ), cmd!("home.autofitRowHeight", "AutoFit Row Height", ["Home", "Cells", "Format"], None, "{rows?}", has_doc, autofit_rows), cmd!("home.autofitColumnWidth", "AutoFit Column Width", ["Home", "Cells", "Format"], None, "{cols?}", has_doc, autofit_cols), cmd!("home.defaultWidth", "Default Width…", ["Home", "Cells", "Format"], None, "{width: 64}", has_doc, default_width), cmd!("home.hideRows", "Hide Rows", ["Home", "Cells", "Format", "Hide & Unhide"], Some("Cmd+9"), "{rows?}", has_doc, |s, p| hide( s, p, true, true )), cmd!("home.hideColumns", "Hide Columns", ["Home", "Cells", "Format", "Hide & Unhide"], Some("Cmd+0"), "{cols?}", has_doc, |s, p| hide( s, p, false, true )), cmd!("home.unhideRows", "Unhide Rows", ["Home", "Cells", "Format", "Hide & Unhide"], Some("Cmd+Shift+9"), "{rows?}", has_doc, |s, p| hide( s, p, true, false )), cmd!( "home.unhideColumns", "Unhide Columns", ["Home", "Cells", "Format", "Hide & Unhide"], Some("Cmd+Shift+0"), "{cols?}", has_doc, |s, p| hide(s, p, false, false) ), cmd!("home.lockCell", "Lock Cell", ["Home", "Cells", "Format"], None, "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.protection.locked, |st, v| st.protection.locked = v )), ] } const ACCOUNTING: &str = "_(\"$\"* #,##0.00_);_(\"$\"* \\(#,##0.00\\);_(\"$\"* \"-\"??_);_(@_)"; const COMMA: &str = "_(* #,##0.00_);_(* \\(#,##0.00\\);_(* \"-\"??_);_(@_)"; fn with(p: &Json, key: &str, v: Json) -> Json { let mut m = p.as_object().cloned().unwrap_or_default(); m.insert(key.into(), v); Json::Object(m) } /// Applies `f` to the style of every targeted cell (whole rows/columns set the line format too). pub(crate) fn apply_style(s: &mut Session, p: &Json, f: impl Fn(&mut Style)) -> Result { let sheet = target_sheet(s, p)?; let ranges = target_ranges(s, p)?; if s.doc()?.wb.sheet(sheet).is_some_and(|sh| sh.protection.as_ref().is_some_and(|pr| !pr.format_cells)) { return Err(EngineError::Other("The cell or chart you're trying to change is on a protected sheet.".into())); } edit(s, |cx| { let mut cache: std::collections::HashMap = std::collections::HashMap::new(); for r in &ranges { let full_rows = r.is_full_rows(); let full_cols = r.is_full_cols(); if full_rows || full_cols { // Line formats. let lines: Vec = if full_rows && !full_cols { (r.start.row..=r.end.row).collect() } else { (r.start.col..=r.end.col).collect() }; for line in lines { let cur = { let sh = cx.wb.sheet(sheet); let info = if full_rows && !full_cols { sh.and_then(|s| s.rows.get(&line)) } else { sh.and_then(|s| s.cols.get(&line)) }; info.and_then(|i| i.style).unwrap_or_default() }; let new = *cache.entry(cur).or_insert_with(|| cx.wb.styles.derive(cur, &f)); let sh = cx.sheet_mut(sheet)?; let map = if full_rows && !full_cols { &mut sh.rows } else { &mut sh.cols }; map.entry(line).or_default().style = Some(new); } // Existing cells in those lines. let cells: Vec<(CellRef, StyleId)> = cx.wb.sheet(sheet).map(|sh| sh.cells.iter_range(*r).map(|(c, x)| (c, x.style)).collect()).unwrap_or_default(); for (c, st) in cells { let new = *cache.entry(st).or_insert_with(|| cx.wb.styles.derive(st, &f)); cx.sheet_mut(sheet)?.set_style(c, new); } continue; } if r.count() > 4_000_000 { return Err(bad("format", "selection too large to format cell by cell")); } if r.height() <= 100_000 { cx.fit_rows.extend((r.start.row..=r.end.row).map(|row| (sheet, row))); } for c in r.iter() { let st = cx.wb.sheet(sheet).map(|sh| sh.style_id(c)).unwrap_or_default(); let new = *cache.entry(st).or_insert_with(|| cx.wb.styles.derive(st, &f)); if new != st { cx.sheet_mut(sheet)?.set_style(c, new); } } } Ok(Json::Null) }) } /// The style of the active cell (what toggles read). pub(crate) fn active_style(s: &Session) -> Result