From 446bc528ad43864a89d68bd6b574f89e1d6cc194 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:45:57 +0000 Subject: [PATCH] Make layout/master placeholder formatting reach slides Fixes #1487 Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/text/formatting.rs | 32 ++++++++++++++++++++++++ crates/engine/src/tests.rs | 26 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/crates/engine/src/cmd/text/formatting.rs b/crates/engine/src/cmd/text/formatting.rs index 158af096..b58cb69e 100644 --- a/crates/engine/src/cmd/text/formatting.rs +++ b/crates/engine/src/cmd/text/formatting.rs @@ -31,8 +31,10 @@ pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sy return Err(bad("format", SELECT_TEXT_OR_SHAPE)); } let cells = st.selection.cells; + let on_master = editing_master(st); edit_shapes(s, &json!({"ids": ids}), "format", |sh| { let id = sh.id; + let inherited = on_master && sh.ph.is_some(); match &mut sh.kind { ShapeKind::Table(t) => { // Only the selected cells, when a range of them is selected. @@ -48,6 +50,13 @@ pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sy _ => { let body = sh.text.get_or_insert_with(TextBody::default); ed::format_all(body, f); + if inherited { + for l in 0..LIST_LEVELS { + let mut lvl = body.list_style.level(l).cloned().unwrap_or_default(); + f(&mut lvl.run); + body.list_style.set(l, lvl); + } + } } } Ok(()) @@ -58,6 +67,15 @@ pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sy ok() } +/// List-style levels (`lvl1pPr`–`lvl9pPr`) a placeholder passes on to the slides that use it. +const LIST_LEVELS: u8 = 9; + +/// Formatting a placeholder on a layout or master (Slide Master view) also sets its list style, +/// which is what the placeholders on slides inherit; its own runs may be few or none. +fn editing_master(st: &DocState) -> bool { + matches!(st.selection.target, crate::Target::Layout { .. } | crate::Target::Master { .. }) +} + /// What `format_run` formats while editing: the selection, or for a bare caret the word it's in /// (without its trailing space). Empty when the caret is at a word boundary. Insert Hyperlink /// edits this same span, so the dialog reads it too. @@ -128,8 +146,10 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex return Err(bad("format", SELECT_TEXT_OR_SHAPE)); } let cells = st.selection.cells; + let on_master = editing_master(st); edit_shapes(s, &json!({"ids": ids}), "format", |sh| { let id = sh.id; + let inherited = on_master && sh.ph.is_some(); match &mut sh.kind { ShapeKind::Table(t) => { let range = crate::cmd::table::selected_range(cells, id, t); @@ -147,6 +167,18 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::tex body.paragraphs.push(Default::default()); } body.paragraphs.iter_mut().for_each(f); + if inherited { + for l in 0..LIST_LEVELS { + let mut lvl = body.list_style.level(l).cloned().unwrap_or_default(); + let mut p = deckcraft_model::text::Paragraph { level: l, props: lvl.para, ..Default::default() }; + f(&mut p); + // A level change (indent/outdent) means nothing for a list-style level. + if p.level == l { + lvl.para = p.props; + body.list_style.set(l, lvl); + } + } + } } } Ok(()) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 85cbe9d8..5b3dcb03 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5956,3 +5956,29 @@ fn invalid_slide_index_never_falls_back_to_the_selection_in_export_commands() { } let _ = std::fs::remove_dir_all(&dir); } + +#[test] +fn master_layout_placeholder_format_inherits() { + let mut s = Session::new(); + sample::open_sample(&mut s).unwrap(); + let title_of = |shapes: &[model::Shape]| shapes.iter().find(|sh| sh.ph.as_ref().is_some_and(|p| p.kind.is_title())).map(|sh| sh.id.0).unwrap(); + let existing = title_of(&s.doc().unwrap().doc.slides[0].shapes); + s.execute("view.slideMaster", &json!({"master": 0, "layout": 0})).unwrap(); + let layout_title = title_of(&s.doc().unwrap().doc.masters[0].layouts[0].shapes); + s.execute("edit.select", &json!({"ids": [layout_title]})).unwrap(); + s.execute("format.font", &json!({"family": "Montserrat"})).unwrap(); + s.execute("format.align", &json!({"align": "right"})).unwrap(); + let st = s.execute("format.state", &json!({})).unwrap(); + assert_eq!(st["fontRef"], "Montserrat", "layout placeholder: {st}"); + s.execute("view.closeMaster", &json!({})).unwrap(); + s.execute("slide.new", &json!({"layout": "title"})).unwrap(); + let slide = s.doc().unwrap().selection.slide; + let fresh = title_of(&s.doc().unwrap().doc.slides[slide].shapes); + for (index, id) in [(slide, fresh), (0, existing)] { + s.execute("slide.go", &json!({"index": index})).unwrap(); + s.execute("edit.select", &json!({"ids": [id]})).unwrap(); + let st = s.execute("format.state", &json!({})).unwrap(); + assert_eq!(st["fontRef"], "Montserrat", "slide placeholder {id}: {st}"); + assert_eq!(st["align"], "r", "slide placeholder {id}: {st}"); + } +}