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 {...@@ -211,8 +211,8 @@ impl State {
211 }211 }
212 ui.close();212 ui.close();
213 crate::buttons(ui);213 crate::buttons(ui);
214 let ok = ui::button(ui, "ok", "OK").clicked || entered;214 let [cancel, ok] = ui::dialog_buttons(ui, "OK", true);
215 let cancel = ui::button(ui, "cancel", "Cancel").clicked;215 let ok = ok || entered;
216 ui.close();216 ui.close();
217 ui.close();217 ui.close();
218 if let Some((space, title)) = picked218 if let Some((space, title)) = picked
crates/snowbound/src/options.rs+2-2
...@@ -674,8 +674,8 @@ impl State {...@@ -674,8 +674,8 @@ impl State {
674 ..Spec::default()674 ..Spec::default()
675 },675 },
676 );676 );
677 let cancel = ui::button(ui, "cancel", "Cancel").clicked;677 let [cancel, ok] = ui::dialog_buttons(ui, "OK", true);
678 let ok = ui::button(ui, "ok", "OK").clicked || entered;678 let ok = ok || entered;
679 ui.close();679 ui.close();
680 ui.close();680 ui.close();
681 let name = options.user_name.trim();681 let name = options.user_name.trim();
crates/snowbound/src/print.rs+2-2
...@@ -256,9 +256,9 @@ impl State {...@@ -256,9 +256,9 @@ impl State {
256 }256 }
257 });257 });
258 crate::buttons(ui);258 crate::buttons(ui);
259 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
260 let action = if dialog.export { "Export" } else { "Print" };259 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;
262 ui.close();262 ui.close();
263 ui.close();263 ui.close();
264 if !go && !cancel {264 if !go && !cancel {
crates/snowbound/src/properties.rs+2-2
...@@ -185,8 +185,8 @@ impl State {...@@ -185,8 +185,8 @@ impl State {
185 crate::platform::reveal(&dialog.library.location);185 crate::platform::reveal(&dialog.library.location);
186 }186 }
187 crate::buttons(ui);187 crate::buttons(ui);
188 let ok = ui::button(ui, "ok", "OK").clicked || entered;188 let [cancel, ok] = ui::dialog_buttons(ui, "OK", true);
189 let cancel = ui::button(ui, "cancel", "Cancel").clicked;189 let ok = ok || entered;
190 ui.close();190 ui.close();
191 ui.close();191 ui.close();
192 let name = dialog.name.trim();192 let name = dialog.name.trim();
crates/snowbound/src/protection.rs+14-6
...@@ -262,7 +262,6 @@ impl State {...@@ -262,7 +262,6 @@ impl State {
262 dialog,262 dialog,
263 });263 });
264 self.ui.open_popup(id());264 self.ui.open_popup(id());
265 self.ui.set_focus(None);
266 }265 }
267266
268 /// Locks every protected section unlocked this run, as Lock All and Ctrl+Alt+L do.267 /// Locks every protected section unlocked this run, as Lock All and Ctrl+Alt+L do.
...@@ -420,7 +419,8 @@ impl State {...@@ -420,7 +419,8 @@ impl State {
420 },419 },
421 );420 );
422 };421 };
423 let mut buttons: &[&str] = &["Cancel", "OK"];422 // Password Protection's own buttons; the others' are Cancel and OK.
423 let mut buttons: &[&str] = &[];
424 match &mut asking.dialog {424 match &mut asking.dialog {
425 Dialog::Unlock(value, wrong) => {425 Dialog::Unlock(value, wrong) => {
426 text(426 text(
...@@ -502,12 +502,20 @@ impl State {...@@ -502,12 +502,20 @@ impl State {
502 ..Spec::default()502 ..Spec::default()
503 },503 },
504 );504 );
505 let pressed = buttons505 let pressed = if buttons.is_empty() {
506 .iter()506 match ui::dialog_buttons(ui, "OK", true) {
507 .position(|label| ui::button(ui, *label, label).clicked);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 };
508 ui.close();516 ui.close();
509 ui.close();517 ui.close();
510 let chosen = pressed.map(|at| buttons[at]).or(entered.then_some("OK"));518 let chosen = pressed.or(entered.then_some("OK"));
511 let Asking {519 let Asking {
512 library,520 library,
513 path,521 path,
crates/snowbound/src/save_as.rs+3-3
...@@ -134,8 +134,8 @@ impl State {...@@ -134,8 +134,8 @@ impl State {
134 dialog.format = at;134 dialog.format = at;
135 }135 }
136 crate::buttons(ui);136 crate::buttons(ui);
137 let cancel = ui::button(ui, "cancel", "Cancel").clicked;137 let [cancel, go] = ui::dialog_buttons(ui, "Save As", true);
138 let go = ui::button(ui, "go", "Save As").clicked || entered;138 let go = go || entered;
139 ui.close();139 ui.close();
140 ui.close();140 ui.close();
141 if !go && !cancel {141 if !go && !cancel {
...@@ -249,7 +249,7 @@ fn choose(ui: &mut ui::Ui, label: &str, names: &[&str], current: usize) -> Optio...@@ -249,7 +249,7 @@ fn choose(ui: &mut ui::Ui, label: &str, names: &[&str], current: usize) -> Optio
249 "label",249 "label",
250 Spec {250 Spec {
251 size: [px(110.0), px(row)],251 size: [px(110.0), px(row)],
252 text: Some(label),252 text: Some(&format!("{label}:")),
253 ..Spec::default()253 ..Spec::default()
254 },254 },
255 );255 );
crates/snowbound/src/server.rs+4-21
...@@ -825,29 +825,12 @@ impl State {...@@ -825,29 +825,12 @@ impl State {
825 ..Spec::default()825 ..Spec::default()
826 },826 },
827 );827 );
828 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
829 let waiting = matches!(connect.status, Status::Waiting(_));828 let waiting = matches!(connect.status, Status::Waiting(_));
830 let go = match connect.step {829 let [cancel, go] = match connect.step {
831 Step::Browse if connect.opens().is_none() => {830 Step::Browse => ui::dialog_buttons(ui, "Open", connect.opens().is_some()),
832 ui.leaf(831 _ => ui::dialog_buttons(ui, "Connect", true),
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,
850 };832 };
833 let go = go || entered && connect.step != Step::Browse;
851 ui.close();834 ui.close();
852 ui.close();835 ui.close();
853 if cancel {836 if cancel {
crates/snowbound/src/tags.rs+3-4
...@@ -525,8 +525,7 @@ impl State {...@@ -525,8 +525,7 @@ impl State {
525 });525 });
526 ui.close();526 ui.close();
527 crate::buttons(ui);527 crate::buttons(ui);
528 let ok = button(ui, "ok", "OK", list.tags != self.tags);528 let [cancel, ok] = ui::dialog_buttons(ui, "OK", list.tags != self.tags);
529 let cancel = ui::button(ui, "cancel", "Cancel").clicked;
530 ui.close();529 ui.close();
531 if let Some(place) = list.selected {530 if let Some(place) = list.selected {
532 if up {531 if up {
...@@ -870,8 +869,8 @@ fn tag_editor(...@@ -870,8 +869,8 @@ fn tag_editor(
870 );869 );
871 crate::buttons(ui);870 crate::buttons(ui);
872 let named = !tag.label.trim().is_empty();871 let named = !tag.label.trim().is_empty();
873 let ok = button(ui, "ok", "OK", named) || entered && named;872 let [cancel, ok] = ui::dialog_buttons(ui, "OK", named);
874 let cancel = ui::button(ui, "cancel", "Cancel").clicked;873 let ok = ok || entered && named;
875 ui.close();874 ui.close();
876 ui.close();875 ui.close();
877 if ok {876 if ok {
crates/snowbound/src/unpack.rs+2-2
...@@ -168,8 +168,8 @@ impl State {...@@ -168,8 +168,8 @@ impl State {
168 },168 },
169 );169 );
170 crate::buttons(ui);170 crate::buttons(ui);
171 let cancel = ui::button(ui, "cancel", "Cancel").clicked;171 let [cancel, create] = ui::dialog_buttons(ui, "Create", true);
172 let create = ui::button(ui, "create", "Create").clicked || entered;172 let create = create || entered;
173 ui.close();173 ui.close();
174 ui.close();174 ui.close();
175 if browse {175 if browse {
crates/ui/src/lib.rs+1-1
...@@ -16,7 +16,7 @@ pub use list::{List, Row, Rows, list};...@@ -16,7 +16,7 @@ pub use list::{List, Row, Rows, list};
16pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme};16pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme};
17pub use widgets::{17pub use widgets::{
18 PaintedScroller, Scroller, ScrollerPart, Scrollers, badge, badge_width, button, check_box,18 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,
20};20};
2121
22use draw::{22use draw::{
crates/ui/src/widgets.rs+40
...@@ -32,6 +32,46 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {...@@ -32,6 +32,46 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal {
32 ui.leaf(part, spec)32 ui.leaf(part, spec)
33}33}
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
35/// A short tag in the accent's colour, centred in a box `height` tall, as BETA marks a75/// A short tag in the accent's colour, centred in a box `height` tall, as BETA marks a
36/// feature still settling beside its name.76/// feature still settling beside its name.
37pub fn badge(ui: &mut Ui, part: impl Hash, text: &str, height: f32) {77pub fn badge(ui: &mut Ui, part: impl Hash, text: &str, height: f32) {