From d92401a36cb3a706f3f80bd0b6aeb1f5cb789375 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 22:09:50 -0700 Subject: [PATCH] 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 --- crates/snowbound/src/link.rs | 4 +-- crates/snowbound/src/options.rs | 4 +-- crates/snowbound/src/print.rs | 4 +-- crates/snowbound/src/properties.rs | 4 +-- crates/snowbound/src/protection.rs | 20 ++++++++++----- crates/snowbound/src/save_as.rs | 6 ++--- crates/snowbound/src/server.rs | 25 +++---------------- crates/snowbound/src/tags.rs | 7 +++--- crates/snowbound/src/unpack.rs | 4 +-- crates/ui/src/lib.rs | 2 +- crates/ui/src/widgets.rs | 40 ++++++++++++++++++++++++++++++ 11 files changed, 75 insertions(+), 45 deletions(-) diff --git a/crates/snowbound/src/link.rs b/crates/snowbound/src/link.rs index 283cf68b3236db587ecb5efb90862cc649f25ef9..513f84e3b5e333012f39be6869c624597c809baf 100644 --- a/crates/snowbound/src/link.rs +++ b/crates/snowbound/src/link.rs @@ -211,8 +211,8 @@ impl State { } ui.close(); crate::buttons(ui); - let ok = ui::button(ui, "ok", "OK").clicked || entered; - let cancel = ui::button(ui, "cancel", "Cancel").clicked; + let [cancel, ok] = ui::dialog_buttons(ui, "OK", true); + let ok = ok || entered; ui.close(); ui.close(); if let Some((space, title)) = picked diff --git a/crates/snowbound/src/options.rs b/crates/snowbound/src/options.rs index 533525d71b3d1bfdaa6fd16d43dad63a48ab3982..db3c2b2f16f5e1f53b6bad58d296ccc282b16f65 100644 --- a/crates/snowbound/src/options.rs +++ b/crates/snowbound/src/options.rs @@ -674,8 +674,8 @@ impl State { ..Spec::default() }, ); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; - let ok = ui::button(ui, "ok", "OK").clicked || entered; + let [cancel, ok] = ui::dialog_buttons(ui, "OK", true); + let ok = ok || entered; ui.close(); ui.close(); let name = options.user_name.trim(); diff --git a/crates/snowbound/src/print.rs b/crates/snowbound/src/print.rs index c41daa8bac98c72967205ee167872382c8284621..7e4d5a5cd576fa7adfefa3d8448345d2b764d42b 100644 --- a/crates/snowbound/src/print.rs +++ b/crates/snowbound/src/print.rs @@ -256,9 +256,9 @@ impl State { } }); crate::buttons(ui); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; let action = if dialog.export { "Export" } else { "Print" }; - let go = ui::button(ui, "go", action).clicked || entered; + let [cancel, go] = ui::dialog_buttons(ui, action, true); + let go = go || entered; ui.close(); ui.close(); if !go && !cancel { diff --git a/crates/snowbound/src/properties.rs b/crates/snowbound/src/properties.rs index da21d467243ba97d48b6338550e2ff111ced347c..de5a9e3aef14aef02fabafe7b179fb544ef098b4 100644 --- a/crates/snowbound/src/properties.rs +++ b/crates/snowbound/src/properties.rs @@ -185,8 +185,8 @@ impl State { crate::platform::reveal(&dialog.library.location); } crate::buttons(ui); - let ok = ui::button(ui, "ok", "OK").clicked || entered; - let cancel = ui::button(ui, "cancel", "Cancel").clicked; + let [cancel, ok] = ui::dialog_buttons(ui, "OK", true); + let ok = ok || entered; ui.close(); ui.close(); let name = dialog.name.trim(); diff --git a/crates/snowbound/src/protection.rs b/crates/snowbound/src/protection.rs index d0ab31f8de8f18ccd66cd023c56ce6921cead79e..065d12964a7a4c3ff68111ca15b6fd4bdf63de52 100644 --- a/crates/snowbound/src/protection.rs +++ b/crates/snowbound/src/protection.rs @@ -262,7 +262,6 @@ impl State { dialog, }); self.ui.open_popup(id()); - self.ui.set_focus(None); } /// Locks every protected section unlocked this run, as Lock All and Ctrl+Alt+L do. @@ -420,7 +419,8 @@ impl State { }, ); }; - let mut buttons: &[&str] = &["Cancel", "OK"]; + // Password Protection's own buttons; the others' are Cancel and OK. + let mut buttons: &[&str] = &[]; match &mut asking.dialog { Dialog::Unlock(value, wrong) => { text( @@ -502,12 +502,20 @@ impl State { ..Spec::default() }, ); - let pressed = buttons - .iter() - .position(|label| ui::button(ui, *label, label).clicked); + let pressed = if buttons.is_empty() { + match ui::dialog_buttons(ui, "OK", true) { + [true, _] => Some("Cancel"), + [_, true] => Some("OK"), + _ => None, + } + } else { + (buttons.iter()) + .find(|label| ui::button(ui, **label, label).clicked) + .copied() + }; ui.close(); ui.close(); - let chosen = pressed.map(|at| buttons[at]).or(entered.then_some("OK")); + let chosen = pressed.or(entered.then_some("OK")); let Asking { library, path, diff --git a/crates/snowbound/src/save_as.rs b/crates/snowbound/src/save_as.rs index 6e6d683d2813acf85b89aa42d4040be3768a3199..38b496f901dd0a8c3acb992e60d98d35275af865 100644 --- a/crates/snowbound/src/save_as.rs +++ b/crates/snowbound/src/save_as.rs @@ -134,8 +134,8 @@ impl State { dialog.format = at; } crate::buttons(ui); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; - let go = ui::button(ui, "go", "Save As").clicked || entered; + let [cancel, go] = ui::dialog_buttons(ui, "Save As", true); + let go = go || entered; ui.close(); ui.close(); if !go && !cancel { @@ -249,7 +249,7 @@ fn choose(ui: &mut ui::Ui, label: &str, names: &[&str], current: usize) -> Optio "label", Spec { size: [px(110.0), px(row)], - text: Some(label), + text: Some(&format!("{label}:")), ..Spec::default() }, ); diff --git a/crates/snowbound/src/server.rs b/crates/snowbound/src/server.rs index 520b44a6e231999ec867957af8a5129257b39b5c..93cefc4be64f8bc7b6d42f12c6f295992dd8de82 100644 --- a/crates/snowbound/src/server.rs +++ b/crates/snowbound/src/server.rs @@ -825,29 +825,12 @@ impl State { ..Spec::default() }, ); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; let waiting = matches!(connect.status, Status::Waiting(_)); - let go = match connect.step { - Step::Browse if connect.opens().is_none() => { - ui.leaf( - "open", - Spec { - size: [ui::fit(), px(row)], - text: Some("Open"), - color: Some(theme.text_dim), - fill: Some(theme.chip), - radius: 4.0, - pad: [theme.font_size * 0.75, 0.0], - center: true, - role: Some(Role::Button), - ..Spec::default() - }, - ); - false - } - Step::Browse => ui::button(ui, "open", "Open").clicked, - _ => ui::button(ui, "connect", "Connect").clicked || entered, + let [cancel, go] = match connect.step { + Step::Browse => ui::dialog_buttons(ui, "Open", connect.opens().is_some()), + _ => ui::dialog_buttons(ui, "Connect", true), }; + let go = go || entered && connect.step != Step::Browse; ui.close(); ui.close(); if cancel { diff --git a/crates/snowbound/src/tags.rs b/crates/snowbound/src/tags.rs index 73cdffddbe9c5ed81711f35a9718bd7fbd8c5dac..0cac6b187d23052b6bb093e92d007bdf603cfb4b 100644 --- a/crates/snowbound/src/tags.rs +++ b/crates/snowbound/src/tags.rs @@ -525,8 +525,7 @@ impl State { }); ui.close(); crate::buttons(ui); - let ok = button(ui, "ok", "OK", list.tags != self.tags); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; + let [cancel, ok] = ui::dialog_buttons(ui, "OK", list.tags != self.tags); ui.close(); if let Some(place) = list.selected { if up { @@ -870,8 +869,8 @@ fn tag_editor( ); crate::buttons(ui); let named = !tag.label.trim().is_empty(); - let ok = button(ui, "ok", "OK", named) || entered && named; - let cancel = ui::button(ui, "cancel", "Cancel").clicked; + let [cancel, ok] = ui::dialog_buttons(ui, "OK", named); + let ok = ok || entered && named; ui.close(); ui.close(); if ok { diff --git a/crates/snowbound/src/unpack.rs b/crates/snowbound/src/unpack.rs index 2267d6d2048ceab9596a5ac7e52de25be80f0df2..42e58823cb6bea51dbc887b6cbacd9d01dcee9b2 100644 --- a/crates/snowbound/src/unpack.rs +++ b/crates/snowbound/src/unpack.rs @@ -168,8 +168,8 @@ impl State { }, ); crate::buttons(ui); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; - let create = ui::button(ui, "create", "Create").clicked || entered; + let [cancel, create] = ui::dialog_buttons(ui, "Create", true); + let create = create || entered; ui.close(); ui.close(); if browse { diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index c5a839fdd2c2ebd69dbe390de7eb47422704c93a..ec8cfa0a3c5fcbf17eac16f102d85dfdd92074e4 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -16,7 +16,7 @@ pub use list::{List, Row, Rows, list}; pub use theme::{Menu, PopupMotion, Section, Shades, Shadow, Theme}; pub use widgets::{ PaintedScroller, Scroller, ScrollerPart, Scrollers, badge, badge_width, button, check_box, - edit_key, edit_modifiers, password_field, scrollbar, text_field, + dialog_buttons, edit_key, edit_modifiers, password_field, scrollbar, text_field, }; use draw::{ diff --git a/crates/ui/src/widgets.rs b/crates/ui/src/widgets.rs index 75c8a56b8fff87e5cb0f1950fcdc5890b39b0f13..3d4513ef662a5a786f1375a691d85805fa9ee655 100644 --- a/crates/ui/src/widgets.rs +++ b/crates/ui/src/widgets.rs @@ -32,6 +32,46 @@ pub fn button(ui: &mut Ui, part: impl Hash, text: &str) -> Signal { ui.leaf(part, spec) } +/// Cancel and a dialog's default button `text`, which takes no clicks while not `enabled`, +/// in the platform's order: the default last on macOS and GNOME, first on Windows, as their +/// own dialogs place them. The default wears the accent around it. Whether each was clicked, +/// Cancel first. +pub fn dialog_buttons(ui: &mut Ui, text: &str, enabled: bool) -> [bool; 2] { + let default = |ui: &mut Ui| { + let theme = &ui.theme; + let spec = Spec { + flags: if enabled { + Flags::CLICKABLE + } else { + Flags::default() + }, + size: [fit(), px(theme.font_size * 2.0)], + text: Some(text), + color: (!enabled).then_some(theme.text_dim), + fill: Some(theme.chip), + hover_fill: enabled.then(|| theme.hover()), + border: enabled.then_some(theme.accent), + radius: 4.0, + pad: [theme.font_size * 0.75, 0.0], + center: true, + role: Some(Role::Button), + ..Spec::default() + }; + let clicked = ui.leaf("default", spec).clicked; + if !enabled && let Some(node) = ui.access(ui.id("default")) { + node.set_disabled(); + } + clicked + }; + let windows = edit::Platform::CURRENT == edit::Platform::Windows; + let mut chosen = windows && default(ui); + let cancel = button(ui, "cancel", "Cancel").clicked; + if !windows { + chosen = default(ui); + } + [cancel, chosen] +} + /// A short tag in the accent's colour, centred in a box `height` tall, as BETA marks a /// feature still settling beside its name. pub fn badge(ui: &mut Ui, part: impl Hash, text: &str, height: f32) { -- 2.54.0