Fix up merged PRs: one format.tabs and one Tabs dialog for #567/#569 and #637

#569 and #637 each added a `format.tabs` command and a Tabs dialog. Keep one of each:
- format.tabs takes #567/#569's add/move/remove/clear/set plus #637's strict `set`
  (unknown alignment or non-object entries are errors, stops within 0.05 pt collapse,
  last wins) and opens the Tabs dialog when called with no fields, as #637 did.
  A defaultTab-only call no longer rewrites the stops.
- The Tabs dialog is #569's; it now seeds from the dialog's params when Paragraph (or
  format.tabs) opened it, and on OK hands the applied stops back to Paragraph.
- Tests adapted: an empty format.tabs opens the dialog; the Paragraph → Tabs test sets
  stops through #569's dialog fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-14 14:38:03 -04:00
co-authored by Claude Opus 5.5
parent 12df77ed19
commit 7acb9faad1
3 changed files with 11 additions and 4 deletions
+3 -1
View File
@@ -50,7 +50,9 @@ fn tabs_add_move_remove_clear_and_undo() {
assert_eq!(tabs(&mut s).len(), 2);
s.execute("format.tabs", &json!({"clear": true})).unwrap();
assert!(tabs(&mut s).is_empty());
assert!(s.execute("format.tabs", &json!({})).is_err());
s.ui_requests.clear();
s.execute("format.tabs", &json!({})).unwrap();
assert!(matches!(s.ui_requests.last(), Some(crate::UiRequest::Dialog { id, .. }) if id == "tabs"), "no fields opens the Tabs dialog");
assert!(s.execute("format.tabs", &json!({"add": {"pos": f64::MAX}})).is_ok(), "clamped, not a crash");
for i in 0..100 {
s.execute("format.tabs", &json!({"add": {"pos": i as f64 * 3.0}})).unwrap();
+7 -1
View File
@@ -2773,7 +2773,9 @@ const TAB_KINDS: [(&str, &str); 4] = [("l", "Left"), ("ctr", "Center"), ("r", "R
/// list (inches, as PowerPoint's dialog) and applied together with OK.
fn tabs_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
if !d.fields.contains_key("stops") {
let r = app.session.execute("format.ruler", &json!({})).unwrap_or_default();
// Opened from Paragraph (or by `format.tabs` with no fields) the stops come with the
// dialog; otherwise they are read from the caret's paragraph.
let r = if d.params.get("tabs").is_some() { d.params.clone() } else { app.session.execute("format.ruler", &json!({})).unwrap_or_default() };
let stops: Vec<String> = r["tabs"]
.as_array()
.map(|a| a.iter().filter_map(|t| Some(format!("{}:{}", t["pos"].as_f64()? / 72.0, t["align"].as_str().unwrap_or("l")))).collect())
@@ -2846,6 +2848,10 @@ fn tabs_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool {
let dt = num(d, "default", 1.0).clamp(0.02, 56.0) * 72.0;
if let Err(e) = app.run("format.tabs", json!({"set": set, "defaultTab": dt})) {
app.set_status(e);
} else if let Some(ret) = d.params.get_mut("returnParams").and_then(Value::as_object_mut) {
// Back in Paragraph, its Tabs… shows what was just applied.
ret.insert("tabs".into(), json!(set));
ret.insert("defaultTab".into(), json!(dt));
}
}
ok || cancel
+1 -2
View File
@@ -71,8 +71,7 @@ fn tabs_from_paragraph_applies_a_stop_and_returns() {
assert_eq!(h.state().dialog.as_ref().map(|d| d.id.as_str()), Some("tabs"), "status {:?}", h.state().status);
{
let d = h.state_mut().dialog.as_mut().unwrap();
d.params["editing"] = json!(true);
d.params["edit"] = json!([{"pos": 144.0, "align": "ctr"}]);
d.fields.insert("stops".into(), "2:ctr".into());
}
h.get_by_label("OK").click_accesskit();
// The Tabs OK frame restores Paragraph; the next frame fills its fields.