authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 22:09:50-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-03 01:50:33-07:00
logd92401a36cb3a706f3f80bd0b6aeb1f5cb789375
tree287ee12af218f42c20e942aa6cdbddc4763f7504
parent9367f7dcc5c2e41ce870e0579a703718aa2e69bd
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

fix: dialogs place Cancel and their default button as the platform does, the default outlined

Options and Save As put OK last, Notebook Properties, Link and Customize Tags first. Every Cancel and default pair now stands as the platform's own dialogs place it, the default last on macOS and GNOME and first on Windows, the default outlined in the accent as Enter's button. Password Protection takes the keyboard's focus as it opens, and Save As's labels end in colons as the other dialogs' do. Assisted-by: claude-opus-5.5

11 files changed, 75 insertions(+), 45 deletions(-)

crates/snowbound/src/link.rs+2-2
......@@ -211,8 +211,8 @@ impl State {
211211 }
212212 ui.close();
213213 crate::buttons(ui);
214 let ok = ui::button(ui, "ok", "OK").clicked || entered;
215 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
214 let [cancel, ok] = ui::dialog_buttons(ui, "OK", true);
215 let ok = ok || entered;
216216 ui.close();
217217 ui.close();
218218 if let Some((space, title)) = picked
crates/snowbound/src/options.rs+2-2
......@@ -674,8 +674,8 @@ impl State {
674674 ..Spec::default()
675675 },
676676 );
677 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
678 let ok = ui::button(ui, "ok", "OK").clicked || entered;
677 let [cancel, ok] = ui::dialog_buttons(ui, "OK", true);
678 let ok = ok || entered;
679679 ui.close();
680680 ui.close();
681681 let name = options.user_name.trim();
crates/snowbound/src/print.rs+2-2
......@@ -256,9 +256,9 @@ impl State {
256256 }
257257 });
258258 crate::buttons(ui);
259 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
260259 let action = if dialog.export { "Export" } else { "Print" };
261 let go = ui::button(ui, "go", action).clicked || entered;
260 let [cancel, go] = ui::dialog_buttons(ui, action, true);
261 let go = go || entered;
262262 ui.close();
263263 ui.close();
264264 if !go && !cancel {
crates/snowbound/src/properties.rs+2-2
......@@ -185,8 +185,8 @@ impl State {
185185 crate::platform::reveal(&dialog.library.location);
186186 }
187187 crate::buttons(ui);
188 let ok = ui::button(ui, "ok", "OK").clicked || entered;
189 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
188 let [cancel, ok] = ui::dialog_buttons(ui, "OK", true);
189 let ok = ok || entered;
190190 ui.close();
191191 ui.close();
192192 let name = dialog.name.trim();
crates/snowbound/src/protection.rs+14-6
......@@ -262,7 +262,6 @@ impl State {
262262 dialog,
263263 });
264264 self.ui.open_popup(id());
265 self.ui.set_focus(None);
266265 }
267266
268267 /// Locks every protected section unlocked this run, as Lock All and Ctrl+Alt+L do.
......@@ -420,7 +419,8 @@ impl State {
420419 },
421420 );
422421 };
423 let mut buttons: &[&str] = &["Cancel", "OK"];
422 // Password Protection's own buttons; the others' are Cancel and OK.
423 let mut buttons: &[&str] = &[];
424424 match &mut asking.dialog {
425425 Dialog::Unlock(value, wrong) => {
426426 text(
......@@ -502,12 +502,20 @@ impl State {
502502 ..Spec::default()
503503 },
504504 );
505 let pressed = buttons
506 .iter()
507 .position(|label| ui::button(ui, *label, label).clicked);
505 let pressed = if buttons.is_empty() {
506 match ui::dialog_buttons(ui, "OK", true) {
507 [true, _] => Some("Cancel"),
508 [_, true] => Some("OK"),
509 _ => None,
510 }
511 } else {
512 (buttons.iter())
513 .find(|label| ui::button(ui, **label, label).clicked)
514 .copied()
515 };
508516 ui.close();
509517 ui.close();
510 let chosen = pressed.map(|at| buttons[at]).or(entered.then_some("OK"));
518 let chosen = pressed.or(entered.then_some("OK"));
511519 let Asking {
512520 library,
513521 path,
crates/snowbound/src/save_as.rs+3-3
......@@ -134,8 +134,8 @@ impl State {
134134 dialog.format = at;
135135 }
136136 crate::buttons(ui);
137 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
138 let go = ui::button(ui, "go", "Save As").clicked || entered;
137 let [cancel, go] = ui::dialog_buttons(ui, "Save As", true);
138 let go = go || entered;
139139 ui.close();
140140 ui.close();
141141 if !go && !cancel {
......@@ -249,7 +249,7 @@ fn choose(ui: &mut ui::Ui, label: &str, names: &[&str], current: usize) -> Optio
249249 "label",
250250 Spec {
251251 size: [px(110.0), px(row)],
252 text: Some(label),
252 text: Some(&format!("{label}:")),
253253 ..Spec::default()
254254 },
255255 );
crates/snowbound/src/server.rs+4-21
......@@ -825,29 +825,12 @@ impl State {
825825 ..Spec::default()
826826 },
827827 );
828 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
829828 let waiting = matches!(connect.status, Status::Waiting(_));
830 let go = match connect.step {
831 Step::Browse if connect.opens().is_none() => {
832 ui.leaf(
833 "open",
834 Spec {
835 size: [ui::fit(), px(row)],
836 text: Some("Open"),
837 color: Some(theme.text_dim),
838 fill: Some(theme.chip),
839 radius: 4.0,
840 pad: [theme.font_size * 0.75, 0.0],
841 center: true,
842 role: Some(Role::Button),
843 ..Spec::default()
844 },
845 );
846 false
847 }
848 Step::Browse => ui::button(ui, "open", "Open").clicked,
849 _ => ui::button(ui, "connect", "Connect").clicked || entered,
829 let [cancel, go] = match connect.step {
830 Step::Browse => ui::dialog_buttons(ui, "Open", connect.opens().is_some()),
831 _ => ui::dialog_buttons(ui, "Connect", true),
850832 };
833 let go = go || entered && connect.step != Step::Browse;
851834 ui.close();
852835 ui.close();
853836 if cancel {
crates/snowbound/src/tags.rs+3-4
......@@ -525,8 +525,7 @@ impl State {
525525 });
526526 ui.close();
527527 crate::buttons(ui);
528 let ok = button(ui, "ok", "OK", list.tags != self.tags);
529 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
528 let [cancel, ok] = ui::dialog_buttons(ui, "OK", list.tags != self.tags);
530529 ui.close();
531530 if let Some(place) = list.selected {
532531 if up {
......@@ -870,8 +869,8 @@ fn tag_editor(
870869 );
871870 crate::buttons(ui);
872871 let named = !tag.label.trim().is_empty();
873 let ok = button(ui, "ok", "OK", named) || entered && named;
874 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
872 let [cancel, ok] = ui::dialog_buttons(ui, "OK", named);
873 let ok = ok || entered && named;
875874 ui.close();
876875 ui.close();
877876 if ok {
crates/snowbound/src/unpack.rs+2-2
......@@ -168,8 +168,8 @@ impl State {
168168 },
169169 );
170170 crate::buttons(ui);
171 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
172 let create = ui::button(ui, "create", "Create").clicked || entered;
171 let [cancel, create] = ui::dialog_buttons(ui, "Create", true);
172 let create = create || entered;
173173 ui.close();
174174 ui.close();
175175 if browse {
crates/ui/src/lib.rs+1-1
......@@ -16,7 +16,7 @@ pub use list::{List, Row, Rows, list};
1616pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme};
1717pub use widgets::{
1818 PaintedScroller, Scroller, ScrollerPart, Scrollers, badge, badge_width, button, check_box,
19 edit_key, edit_modifiers, password_field, scrollbar, text_field,
19 dialog_buttons, edit_key, edit_modifiers, password_field, scrollbar, text_field,
2020};
2121
2222use draw::{
crates/ui/src/widgets.rs+40
......@@ -32,6 +32,46 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {
3232 ui.leaf(part, spec)
3333}
3434
35/// Cancel and a dialog's default button `text`, which takes no clicks while not `enabled`,
36/// in the platform's order: the default last on macOS and GNOME, first on Windows, as their
37/// own dialogs place them. The default wears the accent around it. Whether each was clicked,
38/// Cancel first.
39pub fn dialog_buttons(ui: &mut Ui, text: &str, enabled: bool) -> [bool; 2] {
40 let default = |ui: &mut Ui| {
41 let theme = &ui.theme;
42 let spec = Spec {
43 flags: if enabled {
44 Flags::CLICKABLE
45 } else {
46 Flags::default()
47 },
48 size: [fit(), px(theme.font_size * 2.0)],
49 text: Some(text),
50 color: (!enabled).then_some(theme.text_dim),
51 fill: Some(theme.chip),
52 hover_fill: enabled.then(|| theme.hover()),
53 border: enabled.then_some(theme.accent),
54 radius: 4.0,
55 pad: [theme.font_size * 0.75, 0.0],
56 center: true,
57 role: Some(Role::Button),
58 ..Spec::default()
59 };
60 let clicked = ui.leaf("default", spec).clicked;
61 if !enabled && let Some(node) = ui.access(ui.id("default")) {
62 node.set_disabled();
63 }
64 clicked
65 };
66 let windows = edit::Platform::CURRENT == edit::Platform::Windows;
67 let mut chosen = windows && default(ui);
68 let cancel = button(ui, "cancel", "Cancel").clicked;
69 if !windows {
70 chosen = default(ui);
71 }
72 [cancel, chosen]
73}
74
3575/// A short tag in the accent's colour, centred in a box `height` tall, as BETA marks a
3676/// feature still settling beside its name.
3777pub fn badge(ui: &mut Ui, part: impl Hash, text: &str, height: f32) {