diff --git a/crates/engine/src/cmd/para.rs b/crates/engine/src/cmd/para.rs index 1bb4831..bc2a1bd 100644 --- a/crates/engine/src/cmd/para.rs +++ b/crates/engine/src/cmd/para.rs @@ -1146,3 +1146,29 @@ fn own_format(st: &Style) -> Value { pub fn touch_all(s: &mut Session) { s.dirty = true; } + +/// Which Home › Paragraph list buttons show as on. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ListButtons { + pub bullets: bool, + pub numbering: bool, + pub multilevel: bool, +} + +/// The list buttons for a paragraph with resolved `numbering` (the ribbon passes the paragraph +/// props it already resolved). The paragraph's own level decides Bullets (a bulleted level) or +/// Numbering (a numbered one); Multilevel is on too when the list numbers its levels together: +/// some level's label shows another level's number (`1.1.`) or uses legal numbering. +pub fn list_buttons(doc: &wordcraft_doc::Document, numbering: Option) -> ListButtons { + let Some(n) = numbering.filter(|n| n.num != 0) else { return ListButtons::default() }; + let Some(level) = doc.numbering.level(n.num, n.level) else { return ListButtons::default() }; + let bullets = level.format == wordcraft_doc::section::NumFormat::Bullet; + let joined = |i: u8| { + doc.numbering.level(n.num, i).is_some_and(|l| { + let own = u32::from(i) + 1; + l.legal || l.text.split('%').skip(1).filter_map(|t| t.chars().next()?.to_digit(10)).any(|k| k != own) + }) + }; + let multilevel = (0..9u8).any(joined); + ListButtons { bullets, numbering: !bullets, multilevel } +} diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e292a8f..09b6dbf 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3597,6 +3597,38 @@ fn enclose_characters_make_an_eq_field_and_come_back_out() { assert_eq!(field(&s), Some(("eq \\o\\ac(△,A)".to_string(), "A".to_string()))); } +#[test] +fn list_buttons_follow_the_paragraphs_own_level() { + use crate::cmd::para::{ListButtons, list_buttons}; + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "item"})); + let state = |s: &Session| { + let rp = s.doc.para_at(&s.sel.focus).map(|p| s.doc.styles.resolve_para(&p.props)); + list_buttons(&s.doc, rp.and_then(|r| r.numbering)) + }; + assert_eq!(state(&s), ListButtons::default(), "plain text: no list button on"); + run(&mut s, "para.bullets", json!({})); + assert_eq!(state(&s), ListButtons { bullets: true, numbering: false, multilevel: false }); + // A bulleted list whose second level is numbered: at level 2 the paragraph is numbered. + let num = s.doc.para_at(&s.sel.focus).unwrap().props.numbering.unwrap().num; + let numbered = wordcraft_doc::numbering::Level { text: "%2.".into(), ..Default::default() }; + s.doc.numbering.nums.iter_mut().find(|n| n.id == num).unwrap().level_overrides.push((1, numbered)); + run(&mut s, "para.listLevel", json!({"level": 1})); + assert_eq!(state(&s), ListButtons { bullets: false, numbering: true, multilevel: false }); + run(&mut s, "para.numbering", json!({})); + assert_eq!(state(&s), ListButtons { bullets: false, numbering: true, multilevel: false }, "1. a. i. is not multilevel"); + // Legal numbering (1. 1.1. 1.1.1.) numbers levels together, even on its first level. (Its + // first level reads like "1." numbering, which the button would toggle off, so start plain.) + run(&mut s, "para.numbering", json!({"off": true})); + run(&mut s, "para.multilevel", json!({"kind": "legal"})); + run(&mut s, "para.listLevel", json!({"level": 0})); + assert_eq!(state(&s), ListButtons { bullets: false, numbering: true, multilevel: true }); + run(&mut s, "para.listLevel", json!({"level": 2})); + assert!(state(&s).multilevel); + run(&mut s, "para.numbering", json!({"off": true})); + assert_eq!(state(&s), ListButtons::default()); +} + #[test] fn replace_all_splits_paragraphs_at_a_paragraph_mark_in_the_replacement() { // Issue #423: untracked Replace All left `\r` in the paragraph instead of splitting it. diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index b374376..50b8f8c 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -560,9 +560,10 @@ fn home(app: &mut WordApp, ui: &mut Ui) { // The buttons show alignment as seen on the page (Align Right is a right-to-left paragraph's start). let align = rp.as_ref().map(|r| r.align.visual(r.bidi)).unwrap_or_default(); let rtl = rp.as_ref().is_some_and(|r| r.bidi); + let lists = wordcraft_engine::cmd::para::list_buttons(&app.session.doc, rp.as_ref().and_then(|r| r.numbering)); stack(ui, |ui| { crate::widgets::row(ui, |ui| { - split(ui, app, "bullets", "Bullets", "para.bullets", json!({}), false, None, |ui, app| { + split(ui, app, "bullets", "Bullets", "para.bullets", json!({}), lists.bullets, None, |ui, app| { ui.label(egui::RichText::new(tl!("Bullet Library")).small().weak()); ui.horizontal(|ui| { for c in ["•", "○", "▪", "◆", "➢", "✓", "–"] { @@ -583,7 +584,7 @@ fn home(app: &mut WordApp, ui: &mut Ui) { }); mi(ui, app, "None", "para.bullets", json!({"off": true})); }); - split(ui, app, "numbering", "Numbering", "para.numbering", json!({}), false, None, |ui, app| { + split(ui, app, "numbering", "Numbering", "para.numbering", json!({}), lists.numbering, None, |ui, app| { for (l, k) in [ ("1. 2. 3.", "numbered"), ("1) 2) 3)", "numberedParen"), @@ -598,7 +599,7 @@ fn home(app: &mut WordApp, ui: &mut Ui) { mi(ui, app, "Restart at 1", "para.restartNumbering", json!({})); mi(ui, app, "None", "para.numbering", json!({"off": true})); }); - split(ui, app, "multilevel", "Multilevel List", "para.multilevel", json!({}), false, None, |ui, app| { + split(ui, app, "multilevel", "Multilevel List", "para.multilevel", json!({}), lists.multilevel, None, |ui, app| { mi(ui, app, "1. 1.1. 1.1.1.", "para.multilevel", json!({"kind": "legal"})); mi(ui, app, "I. A. 1. a.", "para.multilevel", json!({"kind": "outline"})); ui.separator();