Merge #1301: Text: a Columns dialog sets the number of columns and the spacing between them

This commit is contained in:
aiongg
2026-10-25 02:22:38 +00:00
8 changed files with 332 additions and 8 deletions
+2
View File
@@ -140,6 +140,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-23 | Format Background ▸ Apply to All also sets the background of the slide masters the slides use (and clears their layouts' overrides), so new slides keep it (#891) |
| 2026-10-22 | Table text takes the master's other-text style (`otherStyle`), shapes and text boxes the presentation's default text style, as PowerPoint for Mac draws them (synthetic probe: 14 pt cells under a 14 pt `otherStyle` and a 26 pt `defaultTextStyle`); a private deck's table had been drawn 20 pt instead of 18 |
| 2026-10-18 | Find (Ctrl+F): "Find what:" has the keyboard as soon as the dialog opens, and keeps it after Enter finds a match, so the search can be typed and changed without clicking the field (#951) |
| 2026-10-24 | Columns dialog: Home › Columns › More Columns… and Format Shape › Text Box › Columns… set the number of text columns (1 to 16) and the spacing between them in inches, in one undo step; `format.columns` with no fields opens it, and a key left out keeps the text box's own value |
| 2026-10-18 | Duplicate (Ctrl+D) repeats a move: duplicate a shape, move the copy, and the next Duplicate places its copy the same distance further on, as in PowerPoint (#966) |
| 2026-10-22 | Slide Show: while animations play, each frame draws only the slide's shapes over the slide's backdrop (background, master and layout graphics), rendered once and ahead of time. A full-slide picture with a glow in the layout re-rendered at every frame held animations to 2–3 frames a second (~390 ms a frame at 1080p); now ~28 ms |
| 2026-10-22 | Animation Pane ▸ Play From: with an effect selected, the pane's play button starts the canvas preview at that effect, with the effects before it already played. With nothing selected it is still Play All |
@@ -283,6 +284,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-24 | trivial | Progress log: selecting a range of table cells by dragging. Scores and milestone names unchanged |
| 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged |
| 2026-10-23 | trivial | Progress log: scroll bars on the zoomed slide pane (#1157). Scores and milestone names unchanged |
| 2026-10-24 | trivial | Progress log: the Columns dialog. Scores and milestone names unchanged |
| 2026-10-20 | trivial | Explicit empty chart number-format sections hide their values; blank whole-code General fallback is retained. Synthetic formatter regressions; chart-element scores and readiness estimates unchanged |
| 2026-10-21 | trivial | Progress log: Reuse Slides picker (#1103). Scores and milestone names unchanged |
| 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged |
+91 -5
View File
@@ -198,7 +198,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_text_or_shapes,
direction
),
cmd!("format.columns", "Columns", ["Home", "Paragraph"], None, "{count, spacing?: pt}", has_text_or_shapes, columns),
cmd!(
"format.columns",
"Columns",
["Home", "Paragraph"],
None,
"{count?: 1-16, spacing?: pt between columns}; a key left out keeps the shape's value, and with neither the Columns dialog opens",
has_text_or_shapes,
columns
),
cmd!("format.autofit", "Autofit", ["Format Shape", "Text Box"], None, "{mode: none|shrink|resize}", has_text_or_shapes, autofit),
cmd!("format.wrap", "Wrap Text in Shape", ["Format Shape", "Text Box"], None, "{on: bool}", has_text_or_shapes, wrap),
cmd!(
@@ -1111,12 +1119,45 @@ fn direction(s: &mut Session, p: &Value) -> Result<Value> {
};
format_body(s, &move |b| b.vert = Some(d))
}
/// The edited or first selected shape's columns as drawn: the count and the spacing in points.
fn current_columns(s: &Session) -> (u32, f64) {
let Ok(st) = s.doc() else { return (1, 0.0) };
let id = match &st.selection.text {
Some(t) if !t.notes => Some(t.shape),
_ => st.selection.shapes.first().copied(),
};
let Some(sh) = id.and_then(|id| st.shape(id)) else { return (1, 0.0) };
let body = match super::text::ctx_for(&st.doc, &st.selection) {
Some(c) => resolve::body(&c, sh),
None => sh.text.as_ref().map(|b| b.body.clone()).unwrap_or_default(),
};
(body.columns.unwrap_or(1).clamp(1, 16), body.col_spacing.unwrap_or(0.0).max(0.0))
}
fn columns(s: &mut Session, p: &Value) -> Result<Value> {
let n = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 16) as u32;
let sp = f64_or(p, "spacing", 0.0).max(0.0);
const CMD: &str = "format.columns";
let count = match p.get("count") {
None => None,
Some(v) => {
let n = v
.as_f64()
.filter(|n| n.fract() == 0.0 && (1.0..=16.0).contains(n))
.ok_or_else(|| bad(CMD, "`count` must be a whole number from 1 to 16"))?;
Some(n as u32)
}
};
let spacing = finite_key(p, CMD, "spacing")?.map(clamp_pt);
if count.is_none() && spacing.is_none() {
let (count, spacing) = current_columns(s);
s.ui_requests.push(crate::UiRequest::Dialog { id: "columns".into(), params: json!({"count": count, "spacing": spacing}) });
return ok();
}
format_body(s, &move |b| {
b.columns = Some(n);
b.col_spacing = Some(sp);
if let Some(n) = count {
b.columns = Some(n);
}
if let Some(sp) = spacing {
b.col_spacing = Some(sp);
}
})
}
fn autofit(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1615,6 +1656,51 @@ mod tests {
assert!(para(&s).indent.is_none());
}
fn body(s: &Session) -> deckcraft_model::text::BodyProps {
s.doc().unwrap().selected_shapes().first().and_then(|sh| sh.text.as_ref()).map(|t| t.body.clone()).unwrap_or_default()
}
#[test]
fn columns_set_the_number_and_spacing_and_a_left_out_key_keeps_its_value() {
let mut s = shape();
let n = s.doc().unwrap().history.undo.len();
s.execute("format.columns", &json!({"count": 4, "spacing": 36.0})).unwrap();
assert_eq!((body(&s).columns, body(&s).col_spacing), (Some(4), Some(36.0)));
assert_eq!(s.doc().unwrap().history.undo.len(), n + 1, "one undo step");
s.execute("format.columns", &json!({"count": 2})).unwrap();
assert_eq!((body(&s).columns, body(&s).col_spacing), (Some(2), Some(36.0)), "the spacing stays");
s.execute("format.columns", &json!({"spacing": 9.0})).unwrap();
assert_eq!((body(&s).columns, body(&s).col_spacing), (Some(2), Some(9.0)), "the number stays");
}
#[test]
fn columns_with_no_fields_open_the_dialog_with_the_shapes_values() {
let mut s = shape();
s.execute("format.columns", &json!({"count": 3, "spacing": 27.0})).unwrap();
let rev = s.doc().unwrap().revision;
let n = s.doc().unwrap().history.undo.len();
s.ui_requests.clear();
s.execute("format.columns", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().revision, rev);
assert_eq!(s.doc().unwrap().history.undo.len(), n);
let Some(UiRequest::Dialog { id, params }) = s.ui_requests.last() else { panic!("dialog request") };
assert_eq!(id, "columns");
assert_eq!(params["count"], 3);
assert_eq!(params["spacing"], 27.0);
}
#[test]
fn columns_reject_a_bad_number_or_spacing_without_editing() {
let mut s = shape();
let n = s.doc().unwrap().history.undo.len();
for p in [json!({"count": 0}), json!({"count": 17}), json!({"count": 2.5}), json!({"count": "two"}), json!({"spacing": "wide"})] {
let err = s.execute("format.columns", &p).unwrap_err().to_string();
assert!(err.contains("format.columns"), "{p}: {err}");
}
assert_eq!(s.doc().unwrap().history.undo.len(), n);
assert_eq!(body(&s).columns, None);
}
#[test]
fn tabs_dialog_sets_stops_in_one_undo_and_opens_with_no_fields() {
let mut s = shape();
+167
View File
@@ -0,0 +1,167 @@
//! The Columns dialog: Home › Columns › More Columns… and Format Shape › Text Box › Columns… open it
//! with the text box's values, and OK sets the number and spacing in one undo step.
use deckcraft_engine::Session;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use crate::{Services, SlideApp};
fn harness() -> Harness<'static, SlideApp> {
let mut s = Session::with_new();
s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 320.0, 160.0], "text": "Hello"})).unwrap();
let _ = s.execute("text.exit", &json!({}));
harness_of(s)
}
/// Two text boxes with different columns, both selected: 2 columns 9 pt apart, and 3 columns
/// 36 pt apart.
fn two_boxes() -> Harness<'static, SlideApp> {
let mut s = Session::with_new();
let mut ids = Vec::new();
for (y, count, spacing) in [(80.0, 2, 9.0), (300.0, 3, 36.0)] {
ids.push(s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, y, 320.0, 160.0], "text": "Hello"})).unwrap()["id"].clone());
let _ = s.execute("text.exit", &json!({}));
s.execute("format.columns", &json!({"count": count, "spacing": spacing})).unwrap();
}
s.execute("edit.select", &json!({"ids": ids})).unwrap();
harness_of(s)
}
fn harness_of(s: Session) -> Harness<'static, SlideApp> {
let app = SlideApp::new(s, Services::default());
let mut h = Harness::builder().with_size(egui::vec2(1440.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(4);
h
}
fn columns_of(h: &Harness<'_, SlideApp>) -> (Option<u32>, Option<f64>) {
let st = h.state().session.active().unwrap();
let body = st.selected_shapes().first().and_then(|sh| sh.text.as_ref()).map(|t| t.body.clone()).unwrap_or_default();
(body.columns, body.col_spacing)
}
fn columns_of_each(h: &Harness<'_, SlideApp>) -> Vec<(Option<u32>, Option<f64>)> {
let st = h.state().session.active().unwrap();
st.selected_shapes().iter().map(|sh| sh.text.as_ref().map(|t| (t.body.columns, t.body.col_spacing)).unwrap_or_default()).collect()
}
/// Opens the dialog on `h`'s selection, types `fields` into it and presses OK.
fn ok_with(h: &mut Harness<'_, SlideApp>, fields: &[(&str, &str)]) {
h.state_mut().run("format.columns", json!({})).unwrap();
h.run_steps(2);
let d = h.state_mut().dialog.as_mut().unwrap();
for (key, value) in fields {
d.fields.insert((*key).into(), (*value).into());
}
h.get_by_label("OK").click_accesskit();
h.run_steps(2);
assert!(h.state().dialog.is_none(), "status {:?}", h.state().status);
}
/// With text boxes whose columns differ, the dialog shows the first one's values, and a field
/// left as shown does not overwrite the others' own values.
#[test]
fn a_field_left_as_shown_keeps_each_text_box_s_own_value() {
let mut h = two_boxes();
assert_eq!(columns_of_each(&h), [(Some(2), Some(9.0)), (Some(3), Some(36.0))]);
ok_with(&mut h, &[("count", "4")]);
assert_eq!(columns_of_each(&h), [(Some(4), Some(9.0)), (Some(4), Some(36.0))], "Number only");
let mut h = two_boxes();
ok_with(&mut h, &[("spacing", "0.5")]);
assert_eq!(columns_of_each(&h), [(Some(2), Some(36.0)), (Some(3), Some(36.0))], "Spacing only");
}
#[test]
fn ok_with_nothing_changed_closes_without_an_edit() {
let mut h = two_boxes();
let n = h.state().session.active().unwrap().history.undo.len();
ok_with(&mut h, &[]);
assert_eq!(h.state().session.active().unwrap().history.undo.len(), n);
assert_eq!(columns_of_each(&h), [(Some(2), Some(9.0)), (Some(3), Some(36.0))]);
}
fn field(h: &Harness<'_, SlideApp>, key: &str) -> Option<String> {
h.state().dialog.as_ref().and_then(|d| d.fields.get(key).cloned())
}
#[test]
fn more_columns_sets_the_number_and_spacing_in_one_undo() {
let mut h = harness();
h.state_mut().session.execute("format.columns", &json!({"count": 3, "spacing": 9.0})).unwrap();
h.run_steps(2);
h.get_by_label("Columns").click();
h.run_steps(2);
h.get_by_label("More Columns…").click();
h.run_steps(2);
assert_eq!(h.state().dialog.as_ref().map(|d| d.id.as_str()), Some("columns"), "status {:?}", h.state().status);
assert_eq!(field(&h, "count").as_deref(), Some("3"));
assert_eq!(field(&h, "spacing").as_deref(), Some("0.12"));
{
let d = h.state_mut().dialog.as_mut().unwrap();
d.fields.insert("count".into(), "2".into());
d.fields.insert("spacing".into(), "0.5".into());
}
let n = h.state().session.active().unwrap().history.undo.len();
h.get_by_label("OK").click_accesskit();
h.run_steps(2);
assert!(h.state().dialog.is_none(), "status {:?}", h.state().status);
assert_eq!(columns_of(&h), (Some(2), Some(36.0)));
let undo = &h.state().session.active().unwrap().history.undo;
assert_eq!(undo.len(), n + 1);
assert_eq!(undo.last().unwrap().label, "Columns");
}
#[test]
fn ok_with_the_spacing_unchanged_keeps_the_exact_spacing() {
let mut h = harness();
h.state_mut().session.execute("format.columns", &json!({"count": 3, "spacing": 9.0})).unwrap();
h.state_mut().run("format.columns", json!({})).unwrap();
h.run_steps(2);
assert_eq!(field(&h, "spacing").as_deref(), Some("0.12"), "shown rounded to two places");
h.state_mut().dialog.as_mut().unwrap().fields.insert("count".into(), "4".into());
h.get_by_label("OK").click_accesskit();
h.run_steps(2);
assert!(h.state().dialog.is_none(), "status {:?}", h.state().status);
assert_eq!(columns_of(&h), (Some(4), Some(9.0)));
}
#[test]
fn format_shape_columns_button_opens_the_dialog() {
let mut h = harness();
h.state_mut().ui.pane = Some("format".into());
h.state_mut().ui.format_tab = "text".into();
h.run_steps(2);
h.get_by_label("Columns…").click();
h.run_steps(2);
assert_eq!(h.state().dialog.as_ref().map(|d| d.id.as_str()), Some("columns"), "status {:?}", h.state().status);
assert_eq!(field(&h, "count").as_deref(), Some("1"));
assert_eq!(field(&h, "spacing").as_deref(), Some("0"));
}
#[test]
fn a_bad_number_keeps_the_dialog_open_and_changes_nothing() {
let mut h = harness();
h.state_mut().run("format.columns", json!({})).unwrap();
h.run_steps(2);
for (count, spacing) in [("0", "0.25"), ("17", "0.25"), ("two", "0.25"), ("2", "-1"), ("2", "wide")] {
{
let d = h.state_mut().dialog.as_mut().unwrap();
d.fields.insert("count".into(), count.into());
d.fields.insert("spacing".into(), spacing.into());
}
h.get_by_label("OK").click_accesskit();
h.run_steps(2);
assert_eq!(h.state().dialog.as_ref().map(|d| d.id.as_str()), Some("columns"), "{count} / {spacing}");
assert!(field(&h, "error").is_some_and(|e| !e.is_empty()), "{count} / {spacing}");
assert_eq!(columns_of(&h), (None, None), "{count} / {spacing}");
}
}
+61
View File
@@ -52,6 +52,7 @@ fn title(id: &str) -> &'static str {
"preferences" => "Preferences",
"autocorrect" => "AutoCorrect",
"gridOptions" => "Grid Options",
"columns" => "Columns",
"export" => "Export",
"symbol" => "Symbol",
"photoAlbum" => "Photo Album",
@@ -2248,6 +2249,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ok || cancel
}
"gridOptions" => grid_options(app, ui, d),
"columns" => columns_dialog(app, ui, d),
"preferences" => {
// The controls apply as they change, so the dialog remembers what it opened with and
// Cancel (or Esc, or the close button: see `show`) puts that back.
@@ -2663,6 +2665,61 @@ fn grid_options(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
cancel
}
/// The Columns dialog's changed fields as `format.columns` params: Number and Spacing (inches,
/// sent in points). A field left as the dialog showed it is left out, so each selected text box
/// keeps its own value; `None` when neither field changed.
fn columns_value(d: &Dialog, shown_count: &str, shown_spacing: &str) -> Result<Option<Value>, String> {
let mut p = serde_json::Map::new();
let count = field_of(d, "count");
if count != shown_count {
let n = count.trim().parse::<u32>().ok().filter(|n| (1..=16).contains(n)).ok_or("Number must be a whole number from 1 to 16")?;
p.insert("count".into(), json!(n));
}
let spacing = field_of(d, "spacing");
if spacing != shown_spacing {
let inches = spacing
.trim()
.trim_end_matches("in")
.trim_end_matches('"')
.trim()
.parse::<f64>()
.ok()
.filter(|v| v.is_finite() && (0.0..=22.0).contains(v))
.ok_or("Spacing must be from 0 to 22 inches")?;
p.insert("spacing".into(), json!(inches * 72.0));
}
Ok((!p.is_empty()).then_some(Value::Object(p)))
}
/// Columns: the number of columns in the text box and the space between them. OK applies one
/// `format.columns` call with the fields that changed, or none when neither did; Cancel leaves
/// the text box as it was.
fn columns_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let count = d.params.get("count").and_then(Value::as_u64).unwrap_or(1).to_string();
let spacing = num_text(d.params.get("spacing").and_then(Value::as_f64).unwrap_or(0.0) / 72.0);
field(ui, d, "Number:", "count", &count);
field(ui, d, "Spacing (in):", "spacing", &spacing);
if let Some(err) = d.fields.get("error").cloned().filter(|s| !s.is_empty()) {
ui.add_space(4.0);
ui.label(egui::RichText::new(err).color(Color32::from_rgb(0xB0, 0x30, 0x30)));
}
let (ok, cancel) = buttons(ui, "OK");
if ok {
let applied = columns_value(d, &count, &spacing).and_then(|p| match p {
Some(p) => app.run("format.columns", p).map(|_| ()),
None => Ok(()),
});
match applied {
Ok(()) => return true,
Err(e) => {
d.fields.insert("error".into(), e);
return false;
}
}
}
cancel
}
fn pretty_equation(s: &str) -> String {
let sup = |c: char| match c {
'0' => '⁰',
@@ -3379,6 +3436,10 @@ mod font_dialog_tests;
#[path = "size_position_tests.rs"]
mod size_position_tests;
#[cfg(test)]
#[path = "columns_dialog_tests.rs"]
mod columns_dialog_tests;
#[cfg(test)]
#[path = "photo_album_tests.rs"]
mod photo_album_tests;
+3
View File
@@ -415,6 +415,9 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
run(app, "format.columns", json!({"count": n, "spacing": 18}));
}
}
if ui.small_button("Columns…").clicked() {
run(app, "format.columns", json!({}));
}
});
}
"picture" => {
+3
View File
@@ -767,6 +767,9 @@ fn paragraph_group(app: &mut SlideApp, ui: &mut Ui, st: &Value) {
run(app, "format.columns", json!({"count": n, "spacing": 18}));
}
}
if ui.button("More Columns…").clicked() {
run(app, "format.columns", json!({}));
}
});
});
ui.add_space(3.0);
+3 -2
View File
@@ -293,8 +293,8 @@ Detail in [ui-parity.md](ui-parity.md).
- **Window chrome** (#71 macOS controls misaligned, #26 GNOME title bar, #41 system light/dark).
2–4 h.
- **Icon consistency across Craft apps** (#73, #56). 2–4 h; coordinate in craftrules.
- **Dialogs**: 41 dialogs vs PowerPoint's ~60. In since the d556ff9 count of 27: Font, Paragraph
and Tabs, Bullets and Numbering, Find and Replace, Replace Fonts, Action Settings, Size and
- **Dialogs**: 42 dialogs vs PowerPoint's ~60. In since the d556ff9 count of 27: Font, Paragraph
and Tabs, Columns, Bullets and Numbering, Find and Replace, Replace Fonts, Action Settings, Size and
Position, Date and Time, Photo Album (Edit Photo Album reopens it), Create New Theme Colors and
Create New Theme Fonts (each edits the open master's scheme, not a saved library; theme fonts
are latin only), Grid Options, AutoCorrect and Paste Special. Format Background picture options
@@ -360,6 +360,7 @@ Detail in [localization-parity.md](localization-parity.md).
|---|---|---|
| 2026-10-24 | trivial | AutoFormat starts bulleted and numbered lists as you type (`* `, `-- `, `> `, `1. `, `a) ` and the rest), with an AutoCorrect option to turn it off |
| 2026-10-22 | minor | Animation Pane ▸ Play From starts the preview at the selected effect; gap 10 keeps the effect gallery (7–13 h) |
| 2026-10-24 | trivial | Columns dialog (number and spacing of text columns); dialog count 41 → 42 |
| 2026-10-20 | trivial | Explicit empty chart number-format sections hide their values; blank whole-code General fallback is retained. Synthetic formatter regressions; chart-element scores and readiness estimates unchanged |
| 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged |
| 2026-10-15 | trivial | Native saves retain chart package parts and relationships, including charts in groups; edited chart data still does not export a stale source workbook. Chart editing gaps and scores remain unchanged |
+2 -1
View File
@@ -72,7 +72,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
| Comments pane (threads) | yes | yes: every slide, with Previous and Next on the Review tab |
| Animation Pane | list + advanced timeline | list, reorder, timing, an advanced timeline with draggable bars, play the current slide on the canvas, and Play From starts at the selected effect |
| Design Ideas (Designer) | yes (cloud) | no |
| Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Photo Album, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 41 (`crates/ui-egui/src/dialogs.rs`): Font, Paragraph, Tabs, Bullets and Numbering, Find and Replace, Replace Fonts, Table, Slide Size, Size and Position, Header and Footer, Date and Time, Hyperlink (address, slide and ScreenTip follow the selection), Action Settings, Set Up Show, Zoom (percent or Fit, applies on OK), Comment, Alt Text (decorative follows the selection), Outline, Rename (section, shape, layout), About, Preferences, AutoCorrect, Grid Options, Export, Symbol, Photo Album (Edit Photo Album reopens this dialog), Paste Special, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media (start, end and fades follow the selection), SmartArt chooser, Create New Theme Colors, Create New Theme Fonts, Quit, Notes Pages |
| Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Photo Album, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 42 (`crates/ui-egui/src/dialogs.rs`): Font, Paragraph, Tabs, Columns (number and spacing; Home › Columns › More Columns… and Format Shape › Text Box › Columns…), Bullets and Numbering, Find and Replace, Replace Fonts, Table, Slide Size, Size and Position, Header and Footer, Date and Time, Hyperlink (address, slide and ScreenTip follow the selection), Action Settings, Set Up Show, Zoom (percent or Fit, applies on OK), Comment, Alt Text (decorative follows the selection), Outline, Rename (section, shape, layout), About, Preferences, AutoCorrect, Grid Options, Export, Symbol, Photo Album (Edit Photo Album reopens this dialog), Paste Special, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media (start, end and fades follow the selection), SmartArt chooser, Create New Theme Colors, Create New Theme Fonts, Quit, Notes Pages |
| Design ▸ Effects | named theme effect schemes | DeckCraft, Soft, Crisp, Lift, Glow and Plain (`design.effects`); colours and fonts stay |
## Direct manipulation
@@ -171,6 +171,7 @@ consistency is open (#73, #56).
|---|---|---|
| 2026-10-24 | trivial | Table cells: dragging across cells selects a range of cells |
| 2026-10-23 | trivial | Scroll bars on the zoomed slide pane (#1157) |
| 2026-10-24 | trivial | Columns dialog: the number of text columns and the spacing between them. Dialog count 41 → 42 |
| 2026-10-23 | minor | Shape Fill and Shape Outline (Home and Shape Format): the button applies the last colour picked from its menu, Accent 1 and Text 1 until one is picked. Line weight and dashes are not changed by the button; Font Color and Highlight stay as they were (#1080) |
| 2026-10-22 | trivial | Group fill and outline commands reach members, including nested groups; selection and single-step undo are preserved (#1079). The existing Arrange / Group checklist score stays D |
| 2026-10-16 | minor | Export › Change File Type › PNG and the Export… dialog offer a transparent background (#784) |