Skip to main content

ravenrs_sheet/engine/cmd/
view.rs

1//! View tab (sheet view state that's saved with the file) and Page Layout settings.
2
3use crate::base::RangeRef;
4use crate::model::Orientation;
5use serde_json::{Value as Json, json};
6
7use super::{
8    CommandSpec, EngineError, Result, Session, always, bad, bool_param, cell_param, cell_param_on, cmd, edit, f64_param, has_doc, str_param,
9    target_range, target_sheet, u32_param,
10};
11
12pub fn specs() -> Vec<CommandSpec> {
13    vec![
14        cmd!(
15            "view.freezePanes",
16            "Freeze Panes",
17            ["View", "Window", "Freeze Panes"],
18            None,
19            "{sheet?, cell?: \"B2\" (rows above and columns left of it freeze); without `cell`, unfreezes a frozen sheet}",
20            has_doc,
21            freeze_panes
22        ),
23        cmd!("view.freezeTopRow", "Freeze Top Row", ["View", "Window", "Freeze Panes"], None, "{sheet?}", has_doc, |s, p| set_freeze(
24            s,
25            target_sheet(s, p)?,
26            Some((1, 0))
27        )),
28        cmd!("view.freezeFirstColumn", "Freeze First Column", ["View", "Window", "Freeze Panes"], None, "{sheet?}", has_doc, |s, p| set_freeze(
29            s,
30            target_sheet(s, p)?,
31            Some((0, 1))
32        )),
33        cmd!("view.unfreezePanes", "Unfreeze Panes", ["View", "Window", "Freeze Panes"], None, "{sheet?}", has_doc, |s, p| set_freeze(
34            s,
35            target_sheet(s, p)?,
36            None
37        )),
38        cmd!("view.gridlines", "Gridlines", ["View", "Show"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "gridlines")),
39        cmd!("view.headings", "Headings", ["View", "Show"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "headings")),
40        cmd!("view.showZeros", "Show Zeros", [], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "zeros")),
41        cmd!("view.showOutlineSymbols", "Show Outline Symbols", [], Some("Ctrl+8"), "{on?}", has_doc, |s, p| sheet_flag(s, p, "outlineSymbols")),
42        cmd!("view.showObjects", "Show Objects", [], Some("Ctrl+6"), "{on?}", has_doc, show_objects),
43        cmd!(
44            "view.showSheetTabs",
45            "Show Sheet Tabs",
46            [],
47            None,
48            "{on?} (File › Options › Advanced, this workbook; toggles without `on`)",
49            has_doc,
50            |s, p| { workbook_view_flag(s, p, "sheetTabs") }
51        ),
52        cmd!(
53            "view.showHorizontalScrollBar",
54            "Show Horizontal Scroll Bar",
55            [],
56            None,
57            "{on?} (this workbook; toggles without `on`)",
58            has_doc,
59            |s, p| { workbook_view_flag(s, p, "horizontal") }
60        ),
61        cmd!("view.showVerticalScrollBar", "Show Vertical Scroll Bar", [], None, "{on?} (this workbook; toggles without `on`)", has_doc, |s, p| {
62            workbook_view_flag(s, p, "vertical")
63        }),
64        cmd!("view.rightToLeft", "Sheet Right-to-Left", ["Page Layout", "Sheet Options"], None, "{on?}", has_doc, |s, p| sheet_flag(s, p, "rtl")),
65        cmd!("view.zoom", "Zoom", ["View", "Zoom"], None, "{percent: 10..400}", has_doc, zoom),
66        cmd!("view.zoom100", "100%", ["View", "Zoom"], None, "{}", has_doc, |s, _| zoom(s, &json!({"percent": 100}))),
67        cmd!(query "view.formulaBar", "Formula Bar", ["View", "Show"], None, "{on?: bool}", always, |s, p| {
68            s.prefs.formula_bar = bool_param(p, "on").unwrap_or(!s.prefs.formula_bar);
69            Ok(json!({"on": s.prefs.formula_bar}))
70        }),
71        cmd!(
72            query "view.statusBar",
73            "Customize Status Bar",
74            [],
75            None,
76            "{average?, count?, numericalCount?, minimum?, maximum?, sum?: bool (the statistics shown for a selection; returns all six)}",
77            always,
78            status_bar
79        ),
80        cmd!("view.zoomToSelection", "Zoom to Selection", ["View", "Zoom"], None, "{viewWidth?, viewHeight?}", has_doc, zoom_selection),
81        cmd!("pageLayout.orientation", "Orientation", ["Page Layout", "Page Setup"], None, "{orientation: portrait|landscape}", has_doc, orientation),
82        cmd!(
83            "pageLayout.size",
84            "Size",
85            ["Page Layout", "Page Setup"],
86            None,
87            "{paper: Letter|Legal|A4|A3|A5|A6|Tabloid|Executive|Ledger|B4|B5|B6|B4 (ISO)|B5 (ISO)|B6 (ISO)|Japanese Postcard|Japanese Double Postcard|Japanese Envelope #3|Japanese Envelope #4|Japanese Envelope Kaku #2|Japanese Envelope Kaku #3 (B4/B5/B6 are the JIS sizes)}",
88            has_doc,
89            paper
90        ),
91        cmd!(
92            "pageLayout.margins",
93            "Margins",
94            ["Page Layout", "Page Setup"],
95            None,
96            "{preset?: normal|wide|narrow, left?, right?, top?, bottom?, header?, footer? (inches)}",
97            has_doc,
98            margins
99        ),
100        cmd!(
101            "pageLayout.printArea",
102            "Set Print Area",
103            ["Page Layout", "Page Setup", "Print Area"],
104            None,
105            "{range?, clear?: bool}",
106            has_doc,
107            print_area
108        ),
109        cmd!(
110            "pageLayout.printTitles",
111            "Print Titles",
112            ["Page Layout", "Page Setup"],
113            None,
114            "{rows?: \"1:1\", cols?: \"A:A\"}",
115            has_doc,
116            print_titles
117        ),
118        cmd!(
119            "pageLayout.scaleToFit",
120            "Scale to Fit",
121            ["Page Layout", "Scale to Fit"],
122            None,
123            "{scale?: 100, width?: pages, height?: pages}",
124            has_doc,
125            scale_fit
126        ),
127        cmd!("pageLayout.printGridlines", "Print Gridlines", ["Page Layout", "Sheet Options"], None, "{on?}", has_doc, |s, p| sheet_flag(
128            s,
129            p,
130            "printGridlines"
131        )),
132        cmd!("pageLayout.printHeadings", "Print Headings", ["Page Layout", "Sheet Options"], None, "{on?}", has_doc, |s, p| sheet_flag(
133            s,
134            p,
135            "printHeadings"
136        )),
137        cmd!(
138            "pageLayout.breaks",
139            "Breaks",
140            ["Page Layout", "Page Setup"],
141            None,
142            "{insert?: bool, remove?: bool, reset?: bool, cell?}",
143            has_doc,
144            breaks
145        ),
146        cmd!(
147            "pageLayout.headerFooter",
148            "Header & Footer",
149            ["Insert", "Text"],
150            None,
151            "{sheet?, header?: \"&C&P\", footer?: \"&CPage &P of &N\", differentFirst?: bool, differentOddEven?: bool, scaleWithDoc?: bool, alignWithMargins?: bool, firstHeader?, firstFooter?, evenHeader?, evenFooter?} or one section: {sheet?, page?: odd|even|first, part: header|footer, section: left|center|right, text: \"Page &[Page] of &[Pages]\"} (field names or codes) → {header, footer, differentFirst, differentOddEven, scaleWithDoc, alignWithMargins, firstHeader, firstFooter, evenHeader, evenFooter}",
152            has_doc,
153            header_footer
154        ),
155        cmd!(
156            "pageLayout.theme",
157            "Themes",
158            ["Page Layout", "Themes"],
159            None,
160            "{name: Craft|Slate|Meadow|Ember|Ocean|Orchid|Graphite, colors?: [12 hex], majorFont?, minorFont?}",
161            has_doc,
162            theme
163        ),
164    ]
165}
166
167fn freeze_panes(s: &mut Session, p: &Json) -> Result<Json> {
168    let si = target_sheet(s, p)?;
169    let d = s.doc()?;
170    let sh = d.wb.sheet(si).ok_or(EngineError::NoDocument)?;
171    if sh.freeze.is_some() && p.get("cell").is_none() {
172        return set_freeze(s, si, None);
173    }
174    let at = match cell_param_on(s, p, "cell", si)? {
175        Some(c) => c,
176        None if si == d.wb.active_sheet() => d.selection.active,
177        // The selection belongs to the active sheet.
178        None => return Err(bad("view.freezePanes", "missing `cell`: the selection is on the active sheet")),
179    };
180    let top = sh.view_top_left;
181    let rows = at.row.saturating_sub(top.row);
182    let cols = at.col.saturating_sub(top.col);
183    let _ = (rows, cols);
184    let (rows, cols) = (at.row, at.col);
185    set_freeze(s, si, if rows == 0 && cols == 0 { None } else { Some((rows, cols)) })
186}
187
188fn set_freeze(s: &mut Session, si: usize, f: Option<(u32, u32)>) -> Result<Json> {
189    edit(s, |cx| {
190        cx.sheet_mut(si)?.freeze = f;
191        Ok(json!({"freeze": f}))
192    })
193}
194
195fn sheet_flag(s: &mut Session, p: &Json, which: &str) -> Result<Json> {
196    edit(s, |cx| {
197        let si = cx.sheet_index();
198        let sh = cx.sheet_mut(si)?;
199        let slot = match which {
200            "gridlines" => &mut sh.show_gridlines,
201            "headings" => &mut sh.show_headings,
202            "zeros" => &mut sh.show_zeros,
203            "outlineSymbols" => &mut sh.show_outline_symbols,
204            "rtl" => &mut sh.right_to_left,
205            "printGridlines" => &mut sh.print.gridlines,
206            _ => &mut sh.print.headings,
207        };
208        *slot = bool_param(p, "on").unwrap_or(!*slot);
209        Ok(json!({"on": *slot}))
210    })
211}
212
213/// File › Options › Advanced › Display options for this workbook: the sheet tabs and the two
214/// scroll bars, saved with the workbook (`<workbookView showSheetTabs … />`).
215fn workbook_view_flag(s: &mut Session, p: &Json, which: &str) -> Result<Json> {
216    edit(s, |cx| {
217        let view = &mut cx.wb.view;
218        let slot = match which {
219            "sheetTabs" => &mut view.show_sheet_tabs,
220            "horizontal" => &mut view.show_horizontal_scroll,
221            _ => &mut view.show_vertical_scroll,
222        };
223        *slot = bool_param(p, "on").unwrap_or(!*slot);
224        Ok(json!({"on": *slot}))
225    })
226}
227
228/// Ctrl+6: hides every object, or shows them again. Placeholders (from a file) count as shown.
229fn show_objects(s: &mut Session, p: &Json) -> Result<Json> {
230    use crate::model::ShowObjects;
231    edit(s, |cx| {
232        let on = bool_param(p, "on").unwrap_or(cx.wb.show_objects == ShowObjects::None);
233        cx.wb.show_objects = if on { ShowObjects::All } else { ShowObjects::None };
234        Ok(json!({"on": on}))
235    })
236}
237
238fn status_bar(s: &mut Session, p: &Json) -> Result<Json> {
239    let st = &mut s.prefs.status_bar;
240    for (key, flag) in [
241        ("average", &mut st.average),
242        ("count", &mut st.count),
243        ("numericalCount", &mut st.numerical_count),
244        ("minimum", &mut st.minimum),
245        ("maximum", &mut st.maximum),
246        ("sum", &mut st.sum),
247    ] {
248        if let Some(on) = bool_param(p, key) {
249            *flag = on;
250        }
251    }
252    Ok(json!({
253        "average": st.average,
254        "count": st.count,
255        "numericalCount": st.numerical_count,
256        "minimum": st.minimum,
257        "maximum": st.maximum,
258        "sum": st.sum,
259    }))
260}
261
262fn zoom(s: &mut Session, p: &Json) -> Result<Json> {
263    let z = f64_param(p, "percent").ok_or_else(|| bad("view.zoom", "missing `percent`"))?.clamp(10.0, 400.0) as u16;
264    let d = s.doc_mut()?;
265    let dirty = d.is_dirty();
266    let wb = std::sync::Arc::make_mut(&mut d.wb);
267    let si = wb.active_sheet();
268    if let Some(sh) = wb.sheet_mut(si) {
269        sh.zoom = z;
270    }
271    if !dirty {
272        d.saved = d.wb.clone();
273    }
274    Ok(json!({"zoom": z}))
275}
276
277fn zoom_selection(s: &mut Session, p: &Json) -> Result<Json> {
278    let d = s.doc()?;
279    let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
280    let r = d.selection.current();
281    let w = (sh.col_left(r.end.col + 1) - sh.col_left(r.start.col)).max(1.0);
282    let h = (sh.row_top(r.end.row + 1) - sh.row_top(r.start.row)).max(1.0);
283    let vw = f64_param(p, "viewWidth").unwrap_or(1200.0);
284    let vh = f64_param(p, "viewHeight").unwrap_or(700.0);
285    let z = ((vw / w).min(vh / h) * 100.0).clamp(10.0, 400.0).floor();
286    zoom(s, &json!({"percent": z}))
287}
288
289fn orientation(s: &mut Session, p: &Json) -> Result<Json> {
290    let o = if str_param(p, "orientation") == Some("landscape") { Orientation::Landscape } else { Orientation::Portrait };
291    edit(s, |cx| {
292        let si = cx.sheet_index();
293        cx.sheet_mut(si)?.print.orientation = o;
294        Ok(Json::Null)
295    })
296}
297
298fn paper(s: &mut Session, p: &Json) -> Result<Json> {
299    let paper = str_param(p, "paper").unwrap_or("Letter").to_string();
300    edit(s, |cx| {
301        let si = cx.sheet_index();
302        cx.sheet_mut(si)?.print.paper = paper.clone();
303        Ok(Json::Null)
304    })
305}
306
307fn margins(s: &mut Session, p: &Json) -> Result<Json> {
308    let preset = match str_param(p, "preset") {
309        Some("wide") => Some([1.0, 1.0, 1.0, 1.0, 0.5, 0.5]),
310        Some("narrow") => Some([0.25, 0.25, 0.75, 0.75, 0.3, 0.3]),
311        Some("normal") => Some([0.7, 0.7, 0.75, 0.75, 0.3, 0.3]),
312        _ => None,
313    };
314    edit(s, |cx| {
315        let si = cx.sheet_index();
316        let m = &mut cx.sheet_mut(si)?.print.margins;
317        if let Some(pr) = preset {
318            *m = pr;
319        }
320        for (i, k) in ["left", "right", "top", "bottom", "header", "footer"].iter().enumerate() {
321            if let Some(v) = f64_param(p, k)
322                && let Some(slot) = m.get_mut(i)
323            {
324                *slot = v.clamp(0.0, 20.0) as f32;
325            }
326        }
327        Ok(Json::Null)
328    })
329}
330
331fn print_area(s: &mut Session, p: &Json) -> Result<Json> {
332    let clear = bool_param(p, "clear").unwrap_or(false);
333    let r = target_range(s, p)?;
334    edit(s, |cx| {
335        let si = cx.sheet_index();
336        cx.sheet_mut(si)?.print.print_area = if clear { None } else { Some(r) };
337        Ok(Json::Null)
338    })
339}
340
341fn print_titles(s: &mut Session, p: &Json) -> Result<Json> {
342    // An absent key leaves that axis alone, and an empty or null value clears it — the same
343    // reading `pageLayout.pageSetup` gives `titleRows`/`titleCols`. Assigning `None` for a
344    // key that was never sent used to wipe the other axis.
345    let axis = |key: &str, is_cols: bool| -> Result<Option<Option<(u32, u32)>>> {
346        match p.get(key) {
347            None => Ok(None),
348            Some(Json::Null) => Ok(Some(None)),
349            Some(Json::String(t)) if t.trim().is_empty() => Ok(Some(None)),
350            Some(Json::String(t)) => match RangeRef::parse(t) {
351                Some(r) => Ok(Some(Some(if is_cols { (r.start.col, r.end.col) } else { (r.start.row, r.end.row) }))),
352                None => Err(bad("pageLayout.printTitles", format!("`{key}` is not a range: `{t}`"))),
353            },
354            Some(_) => Err(bad("pageLayout.printTitles", format!("`{key}` must be text like \"1:2\" or \"A:A\""))),
355        }
356    };
357    let (rows, cols) = (axis("rows", false)?, axis("cols", true)?);
358    edit(s, |cx| {
359        let si = cx.sheet_index();
360        let pr = &mut cx.sheet_mut(si)?.print;
361        if let Some(t) = rows {
362            pr.title_rows = t;
363        }
364        if let Some(t) = cols {
365            pr.title_cols = t;
366        }
367        Ok(Json::Null)
368    })
369}
370
371fn scale_fit(s: &mut Session, p: &Json) -> Result<Json> {
372    edit(s, |cx| {
373        let si = cx.sheet_index();
374        let pr = &mut cx.sheet_mut(si)?.print;
375        if let Some(sc) = f64_param(p, "scale") {
376            pr.scale = sc.clamp(10.0, 400.0) as u16;
377            pr.fit_to = None;
378        }
379        if p.get("width").is_some() || p.get("height").is_some() {
380            pr.fit_to = Some((u32_param(p, "width").unwrap_or(0).min(32767) as u16, u32_param(p, "height").unwrap_or(0).min(32767) as u16));
381        }
382        Ok(Json::Null)
383    })
384}
385
386fn breaks(s: &mut Session, p: &Json) -> Result<Json> {
387    let at = cell_param(s, p, "cell")?.unwrap_or(s.doc()?.selection.active);
388    let insert = bool_param(p, "insert").unwrap_or(false);
389    let remove = bool_param(p, "remove").unwrap_or(false);
390    let reset = bool_param(p, "reset").unwrap_or(false);
391    edit(s, |cx| {
392        let si = cx.sheet_index();
393        let pr = &mut cx.sheet_mut(si)?.print;
394        if reset {
395            pr.row_breaks.clear();
396            pr.col_breaks.clear();
397        } else if insert || !remove {
398            if at.row > 0 && !pr.row_breaks.contains(&at.row) {
399                pr.row_breaks.push(at.row);
400                pr.row_breaks.sort_unstable();
401            }
402            if at.col > 0 && !pr.col_breaks.contains(&at.col) {
403                pr.col_breaks.push(at.col);
404                pr.col_breaks.sort_unstable();
405            }
406        } else {
407            pr.row_breaks.retain(|r| *r != at.row);
408            pr.col_breaks.retain(|c| *c != at.col);
409        }
410        Ok(Json::Null)
411    })
412}
413
414/// Excel keeps at most 255 characters per header or footer.
415const MAX_HEADER_FOOTER: usize = 255;
416
417fn header_footer(s: &mut Session, p: &Json) -> Result<Json> {
418    const CMD: &str = "pageLayout.headerFooter";
419    let si = target_sheet(s, p)?;
420    let cap = |t: &str| t.chars().take(MAX_HEADER_FOOTER).collect::<String>();
421    // One section edited in place (Page Layout view).
422    let section = match (str_param(p, "part"), str_param(p, "section")) {
423        (None, None) => None,
424        (part, sec) => {
425            let footer = match part.unwrap_or("header") {
426                "header" => false,
427                "footer" => true,
428                other => return Err(bad(CMD, format!("`part` must be header or footer, not `{other}`"))),
429            };
430            let idx = match sec.unwrap_or("center") {
431                "left" => 0usize,
432                "center" | "centre" => 1,
433                "right" => 2,
434                other => return Err(bad(CMD, format!("`section` must be left, center or right, not `{other}`"))),
435            };
436            let page = match str_param(p, "page") {
437                None => crate::model::HfPage::Odd,
438                Some(id) => crate::model::HfPage::parse(id).ok_or_else(|| bad(CMD, format!("`page` must be odd, even or first, not `{id}`")))?,
439            };
440            Some((page, footer, idx, super::print::fields_to_codes(str_param(p, "text").unwrap_or(""))))
441        }
442    };
443    edit(s, |cx| {
444        let pr = &mut cx.sheet_mut(si)?.print;
445        if let Some(h) = str_param(p, "header") {
446            pr.header = cap(h);
447        }
448        if let Some(f) = str_param(p, "footer") {
449            pr.footer = cap(f);
450        }
451        for (key, slot) in [
452            ("firstHeader", &mut pr.first_header),
453            ("firstFooter", &mut pr.first_footer),
454            ("evenHeader", &mut pr.even_header),
455            ("evenFooter", &mut pr.even_footer),
456        ] {
457            if let Some(t) = str_param(p, key) {
458                *slot = cap(t);
459            }
460        }
461        if let Some(on) = bool_param(p, "differentFirst") {
462            pr.different_first = on;
463        }
464        if let Some(on) = bool_param(p, "differentOddEven") {
465            pr.different_odd_even = on;
466        }
467        if let Some(on) = bool_param(p, "scaleWithDoc") {
468            pr.hf_scale_with_doc = on;
469        }
470        if let Some(on) = bool_param(p, "alignWithMargins") {
471            pr.hf_align_with_margins = on;
472        }
473        if let Some((page, footer, idx, text)) = &section {
474            let spec = pr.hf_mut(*page, *footer);
475            let mut parts = super::print::split_header_footer(spec);
476            if let Some(slot) = parts.get_mut(*idx) {
477                *slot = text.clone();
478            }
479            *spec = cap(&super::print::join_header_footer(&parts));
480        }
481        Ok(json!({
482            "header": pr.header,
483            "footer": pr.footer,
484            "differentFirst": pr.different_first,
485            "differentOddEven": pr.different_odd_even,
486            "scaleWithDoc": pr.hf_scale_with_doc,
487            "alignWithMargins": pr.hf_align_with_margins,
488            "firstHeader": pr.first_header,
489            "firstFooter": pr.first_footer,
490            "evenHeader": pr.even_header,
491            "evenFooter": pr.even_footer,
492        }))
493    })
494}
495
496/// GridCraft's own theme set (original palettes).
497pub fn themes() -> Vec<(&'static str, [u32; 12])> {
498    vec![
499        ("Craft", [0xFFFFFF, 0x000000, 0xE8E8E8, 0x0E2841, 0x156082, 0xE97132, 0x196B24, 0x0F9ED5, 0xA02B93, 0x4EA72E, 0x467886, 0x96607D]),
500        ("Slate", [0xFFFFFF, 0x1F2328, 0xEEF1F4, 0x3B4652, 0x4F6D8A, 0x8AA1B8, 0xC27C4C, 0x6F9A72, 0x9D6C9B, 0xD3B25A, 0x2F6FB0, 0x7A5C8F]),
501        ("Meadow", [0xFFFFFF, 0x16241A, 0xEEF4EA, 0x2E4A33, 0x4D8B3F, 0x9BC53D, 0xE4B33A, 0x2E8C83, 0xC4643C, 0x7B6A9E, 0x2B7A57, 0x6E7F3A]),
502        ("Ember", [0xFFFFFF, 0x2A1A14, 0xF6EEE8, 0x5A2E1E, 0xD9532B, 0xF2994A, 0xF2C94C, 0x8C3B2E, 0x4A6FA5, 0x6F8F4E, 0xB5472A, 0x8C6A5A]),
503        ("Ocean", [0xFFFFFF, 0x0B1E2D, 0xE8F1F7, 0x123A5A, 0x1F78B4, 0x33A6CC, 0x52C7B8, 0x3B5BA5, 0xF2A65A, 0x8E6CB8, 0x1A6FA8, 0x6A7FB0]),
504        ("Orchid", [0xFFFFFF, 0x24152B, 0xF3EDF6, 0x4B2C5E, 0x8E44AD, 0xC56CF0, 0xF78FB3, 0x3DC1D3, 0xF5CD79, 0x63CDDA, 0x7E3FA0, 0xB066B0]),
505        ("Graphite", [0xFFFFFF, 0x111111, 0xEDEDED, 0x333333, 0x595959, 0x7F7F7F, 0xA5A5A5, 0x3C78D8, 0xCC4125, 0x6AA84F, 0x3C78D8, 0x8E7CC3]),
506    ]
507}
508
509fn theme(s: &mut Session, p: &Json) -> Result<Json> {
510    let name = str_param(p, "name").unwrap_or("Craft").to_string();
511    let colors = p
512        .get("colors")
513        .and_then(Json::as_array)
514        .and_then(|a| {
515            let v: Vec<u32> = a.iter().filter_map(|c| c.as_str().and_then(|h| u32::from_str_radix(h.trim_start_matches('#'), 16).ok())).collect();
516            <[u32; 12]>::try_from(v).ok()
517        })
518        .or_else(|| themes().into_iter().find(|(n, _)| n.eq_ignore_ascii_case(&name)).map(|(_, c)| c))
519        .ok_or_else(|| bad("pageLayout.theme", format!("unknown theme `{name}`")))?;
520    let major = str_param(p, "majorFont").map(str::to_string);
521    let minor = str_param(p, "minorFont").map(str::to_string);
522    edit(s, |cx| {
523        cx.wb.theme.name = name.clone();
524        cx.wb.theme.colors = colors;
525        if let Some(m) = &major {
526            cx.wb.theme.major_font = m.clone();
527        }
528        if let Some(m) = &minor {
529            cx.wb.theme.minor_font = m.clone();
530        }
531        Ok(Json::Null)
532    })
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538
539    #[test]
540    fn zoom_100_resets_the_active_sheet_zoom() {
541        let mut s = Session::new();
542        s.new_workbook();
543        s.execute("view.zoom", json!({"percent": 150})).unwrap();
544        assert_eq!(s.execute("view.zoom100", json!({})).unwrap()["zoom"], 100);
545        assert_eq!(s.doc().unwrap().wb.active().unwrap().zoom, 100);
546    }
547
548    #[test]
549    fn formula_bar_toggles_without_a_workbook_and_defaults_to_shown() {
550        let mut s = Session::new();
551        assert!(s.prefs.formula_bar);
552        assert_eq!(s.execute("view.formulaBar", json!({})).unwrap()["on"], false);
553        assert_eq!(s.execute("view.formulaBar", json!({"on": true})).unwrap()["on"], true);
554        // A prefs.json saved before the option existed keeps the formula bar shown.
555        let old: crate::engine::Prefs = serde_json::from_str(r#"{"userName": "Ann"}"#).unwrap();
556        assert!(old.formula_bar);
557    }
558
559    #[test]
560    fn status_bar_statistics_default_to_average_count_and_sum_and_turn_on_one_at_a_time() {
561        let mut s = Session::new();
562        let all = json!({"average": true, "count": true, "numericalCount": false, "minimum": false, "maximum": false, "sum": true});
563        assert_eq!(s.execute("view.statusBar", json!({})).unwrap(), all);
564        let r = s.execute("view.statusBar", json!({"minimum": true, "sum": false, "count": "yes"})).unwrap();
565        assert_eq!(r, json!({"average": true, "count": true, "numericalCount": false, "minimum": true, "maximum": false, "sum": false}));
566        assert!(s.prefs.status_bar.minimum && !s.prefs.status_bar.sum);
567        // A prefs.json saved before the option existed, or with only some of the six, keeps the defaults for the rest.
568        let old: crate::engine::Prefs = serde_json::from_str(r#"{"userName": "Ann"}"#).unwrap();
569        assert_eq!(old.status_bar, crate::engine::StatusBarStats::default());
570        let part: crate::engine::Prefs = serde_json::from_str(r#"{"statusBar": {"maximum": true}}"#).unwrap();
571        assert!(part.status_bar.maximum && part.status_bar.average && part.status_bar.sum && !part.status_bar.minimum);
572        let saved = serde_json::to_value(&s.prefs).unwrap();
573        assert_eq!(saved["statusBar"]["minimum"], true);
574        assert_eq!(saved["statusBar"]["numericalCount"], false);
575    }
576}