diff --git a/crates/component-shell/tests/window_effects_host.rs b/crates/component-shell/tests/window_effects_host.rs index 606288f1fa..cdce65aee5 100644 --- a/crates/component-shell/tests/window_effects_host.rs +++ b/crates/component-shell/tests/window_effects_host.rs @@ -247,7 +247,9 @@ export default class App extends View { context.update(|window, cx| window.draw(cx).clear(cx)); let tree = context.update(|_, cx| view.read(cx).snapshot().unwrap().debug_tree()); assert!(tree.contains("Errors:1"), "{tree}"); - assert!(tree.contains("Closed:4"), "{tree}"); + // Dialog: on_cancel + on_close; Sheet: on_close; AlertDialog: on_cancel + + // on_close, the same pair a cancelled Dialog reports. + assert!(tree.contains("Closed:5"), "{tree}"); context.update(|window, cx| window.close_dialog(cx)); context.simulate_click(point(px(80.), px(152.)), Modifiers::default()); diff --git a/crates/component/src/dialog/alert_dialog.rs b/crates/component/src/dialog/alert_dialog.rs index c9f86324a4..54ea569334 100644 --- a/crates/component/src/dialog/alert_dialog.rs +++ b/crates/component/src/dialog/alert_dialog.rs @@ -1,10 +1,11 @@ use gpui::{ - AnyElement, App, ClickEvent, IntoElement, ParentElement, Pixels, RenderOnce, StyleRefinement, - Styled, Window, prelude::FluentBuilder as _, + AnyElement, App, ClickEvent, IntoElement, ParentElement, Pixels, RenderOnce, SharedString, + StyleRefinement, Styled, Window, prelude::FluentBuilder as _, }; use crate::{ StyledExt as _, WindowExt as _, + button::ButtonVariant, dialog::{ Dialog, DialogButtonProps, DialogDescription, DialogFooter, DialogHeader, DialogTitle, }, @@ -67,7 +68,6 @@ pub struct AlertDialog { icon: Option, title: Option, description: Option, - button_props: DialogButtonProps, children: Vec, } @@ -85,7 +85,6 @@ impl AlertDialog { icon: None, title: None, description: None, - button_props: DialogButtonProps::default(), children: Vec::new(), } } @@ -94,7 +93,7 @@ impl AlertDialog { /// /// The default of [`AlertDialog`] has OK button. pub fn confirm(mut self) -> Self { - self.button_props.show_cancel = true; + self.base.button_props.show_cancel = Some(true); self } @@ -182,23 +181,51 @@ impl AlertDialog { /// Set the button props of the alert dialog. /// - /// Use this to configure button text, variants, and visibility. + /// Use this to configure button text, variants, and visibility in one + /// value. It overrides only the fields `button_props` sets, so the Cancel + /// button [`Self::confirm`] asked for and callbacks set earlier survive, + /// whatever the call order. For a single property, prefer the direct + /// builders — [`Self::ok_text`], [`Self::ok_variant`], + /// [`Self::cancel_text`], [`Self::cancel_variant`]. /// /// # Examples /// /// ```ignore - /// alert.button_props( + /// alert.confirm().button_props( /// DialogButtonProps::default() /// .ok_text("Delete") /// .ok_variant(ButtonVariant::Danger) /// .cancel_text("Keep") - /// .show_cancel(true) /// ) /// ``` #[track_caller] pub fn button_props(mut self, button_props: DialogButtonProps) -> Self { self.debug_assert_no_trigger(); - self.button_props = button_props; + self.base = self.base.button_props(button_props); + self + } + + /// Sets the text of the OK button. Default is `OK`. + pub fn ok_text(mut self, ok_text: impl Into) -> Self { + self.base.button_props.ok_text = Some(ok_text.into()); + self + } + + /// Sets the variant of the OK button. Default is `ButtonVariant::Primary`. + pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self { + self.base.button_props.ok_variant = Some(ok_variant); + self + } + + /// Sets the text of the Cancel button. Default is `Cancel`. + pub fn cancel_text(mut self, cancel_text: impl Into) -> Self { + self.base.button_props.cancel_text = Some(cancel_text.into()); + self + } + + /// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`. + pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self { + self.base.button_props.cancel_variant = Some(cancel_variant); self } @@ -210,7 +237,7 @@ impl AlertDialog { /// Show cancel button. Default is false. pub fn show_cancel(mut self, show_cancel: bool) -> Self { - self.button_props = self.button_props.show_cancel(show_cancel); + self.base.button_props.show_cancel = Some(show_cancel); self } @@ -250,7 +277,7 @@ impl AlertDialog { mut self, on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static, ) -> Self { - self.button_props = self.button_props.on_ok(on_ok); + self.base = self.base.on_ok(on_ok); self } @@ -261,19 +288,18 @@ impl AlertDialog { mut self, on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static, ) -> Self { - self.button_props = self.button_props.on_cancel(on_cancel); + self.base = self.base.on_cancel(on_cancel); self } /// Build the styled dialog surface around the Base alert-dialog host. pub(crate) fn build_surface(self, window: &mut Window, cx: &mut App) -> Dialog { - let button_props = self.button_props.clone(); + let button_props = self.base.button_props.clone(); let has_title = self.icon.is_some() || self.title.is_some(); let has_header = has_title || self.description.is_some(); let has_footer = self.base.footer.is_some(); self.base - .button_props(button_props.clone()) .when(has_header, |this| { this.header( DialogHeader::new().child( @@ -301,7 +327,7 @@ impl AlertDialog { // Default footer for AlertDialog if user doesn't provide one, with OK and optional Cancel button this.footer( DialogFooter::new() - .when(button_props.show_cancel, |this| { + .when(button_props.is_cancel_shown(), |this| { this.child(button_props.render_cancel(window, cx)) }) .child(button_props.render_ok(window, cx)), @@ -327,8 +353,7 @@ impl AlertDialog { let content_builder = self.base.content_builder.clone(); let style = self.base.style.clone(); let props = self.base.props.clone(); - let mut button_props = self.button_props.clone(); - button_props.on_close = self.base.button_props.on_close.clone(); + let button_props = self.base.button_props.clone(); gpui_base::AlertDialogTrigger::new(trigger) .on_open(move |window, cx| { @@ -364,3 +389,97 @@ impl RenderOnce for AlertDialog { } } } + +#[cfg(test)] +mod tests { + use std::{cell::Cell, rc::Rc}; + + use gpui::{ClickEvent, TestAppContext, px, size}; + + use super::*; + use crate::dialog::dialog::tests::window; + + /// `button_props` overrides only the fields it sets, so the Cancel button + /// `confirm` asked for survives a later props value that never mentions it. + #[gpui::test] + fn button_props_after_confirm_keeps_the_cancel_button(cx: &mut TestAppContext) { + let cx = window(cx, size(px(800.), px(600.))); + cx.update(|_, cx| { + let alert = AlertDialog::new(cx) + .confirm() + .button_props(DialogButtonProps::default().ok_text("Delete")); + + assert!(alert.base.button_props.is_cancel_shown()); + assert_eq!(alert.base.button_props.ok_text.as_deref(), Some("Delete")); + }); + } + + /// The call order does not matter either way round. + #[gpui::test] + fn confirm_after_button_props_keeps_the_ok_text(cx: &mut TestAppContext) { + let cx = window(cx, size(px(800.), px(600.))); + cx.update(|_, cx| { + let alert = AlertDialog::new(cx) + .button_props(DialogButtonProps::default().ok_text("Delete")) + .confirm(); + + assert!(alert.base.button_props.is_cancel_shown()); + assert_eq!(alert.base.button_props.ok_text.as_deref(), Some("Delete")); + }); + } + + /// A callback installed with `on_ok` survives a later props value that + /// carries no callback of its own. + #[gpui::test] + fn button_props_after_on_ok_keeps_the_callback(cx: &mut TestAppContext) { + let cx = window(cx, size(px(800.), px(600.))); + let confirmed = Rc::new(Cell::new(0)); + let counter = confirmed.clone(); + cx.update(|window, cx| { + let alert = AlertDialog::new(cx) + .on_ok(move |_, _, _| { + counter.set(counter.get() + 1); + true + }) + .button_props(DialogButtonProps::default().ok_text("Delete")); + + assert!(alert.base.button_props.ok_handler()( + &ClickEvent::default(), + window, + cx + )); + }); + + assert_eq!(confirmed.get(), 1); + } + + /// The direct builders resolve to the same buttons as a props value. + #[gpui::test] + fn the_direct_builders_match_button_props(cx: &mut TestAppContext) { + let cx = window(cx, size(px(800.), px(600.))); + cx.update(|_, cx| { + let direct = AlertDialog::new(cx) + .confirm() + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) + .cancel_text("Keep") + .cancel_variant(ButtonVariant::Ghost); + let bundled = AlertDialog::new(cx).button_props( + DialogButtonProps::default() + .show_cancel(true) + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) + .cancel_text("Keep") + .cancel_variant(ButtonVariant::Ghost), + ); + + for props in [&direct.base.button_props, &bundled.base.button_props] { + assert!(props.is_cancel_shown()); + assert_eq!(props.ok_text.as_deref(), Some("Delete")); + assert_eq!(props.ok_variant, Some(ButtonVariant::Danger)); + assert_eq!(props.cancel_text.as_deref(), Some("Keep")); + assert_eq!(props.cancel_variant, Some(ButtonVariant::Ghost)); + } + }); + } +} diff --git a/crates/component/src/dialog/dialog.rs b/crates/component/src/dialog/dialog.rs index f5a7313eb6..bd9377e917 100644 --- a/crates/component/src/dialog/dialog.rs +++ b/crates/component/src/dialog/dialog.rs @@ -22,32 +22,35 @@ use crate::{ pub static ANIMATION_DURATION: LazyLock = LazyLock::new(|| Duration::from_secs_f64(0.25)); pub use gpui_base::actions::{Cancel, Confirm}; +type OkHandler = Rc bool + 'static>; +type CancelHandler = Rc bool + 'static>; +type CloseHandler = Rc; + +/// Overwrite `slot` only when `value` was explicitly set. +fn merge_field(slot: &mut Option, value: Option) { + if value.is_some() { + *slot = value; + } +} + /// Dialog button props. -#[derive(Clone)] +/// +/// Every field is unset until a builder sets it, and an unset field falls back +/// to its documented default when the dialog renders. Handing a value to +/// [`Dialog::button_props`] or [`crate::dialog::AlertDialog::button_props`] +/// therefore overrides only the fields that value sets: whatever the dialog +/// already carries — the Cancel button `AlertDialog::confirm` asked for, a +/// callback an earlier `on_ok` installed — survives. +#[derive(Clone, Default)] pub struct DialogButtonProps { pub(crate) ok_text: Option, - pub(crate) ok_variant: ButtonVariant, + pub(crate) ok_variant: Option, pub(crate) cancel_text: Option, - pub(crate) cancel_variant: ButtonVariant, - pub(crate) show_cancel: bool, - pub(crate) on_ok: Rc bool + 'static>, - pub(crate) on_cancel: Rc bool + 'static>, - pub(crate) on_close: Rc, -} - -impl Default for DialogButtonProps { - fn default() -> Self { - Self { - ok_text: None, - ok_variant: ButtonVariant::Primary, - cancel_text: None, - cancel_variant: ButtonVariant::default(), - show_cancel: false, - on_ok: Rc::new(|_, _, _| true), - on_cancel: Rc::new(|_, _, _| true), - on_close: Rc::new(|_, _, _| {}), - } - } + pub(crate) cancel_variant: Option, + pub(crate) show_cancel: Option, + pub(crate) on_ok: Option, + pub(crate) on_cancel: Option, + pub(crate) on_close: Option, } impl DialogButtonProps { @@ -59,7 +62,7 @@ impl DialogButtonProps { /// Sets the variant of the OK button. Default is `ButtonVariant::Primary`. pub fn ok_variant(mut self, ok_variant: ButtonVariant) -> Self { - self.ok_variant = ok_variant; + self.ok_variant = Some(ok_variant); self } @@ -71,13 +74,13 @@ impl DialogButtonProps { /// Sets the variant of the Cancel button. Default is `ButtonVariant::default()`. pub fn cancel_variant(mut self, cancel_variant: ButtonVariant) -> Self { - self.cancel_variant = cancel_variant; + self.cancel_variant = Some(cancel_variant); self } /// Sets whether to show the Cancel button. Default is `false`. pub fn show_cancel(mut self, show_cancel: bool) -> Self { - self.show_cancel = show_cancel; + self.show_cancel = Some(show_cancel); self } @@ -88,7 +91,7 @@ impl DialogButtonProps { mut self, on_ok: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static, ) -> Self { - self.on_ok = Rc::new(on_ok); + self.on_ok = Some(Rc::new(on_ok)); self } @@ -99,10 +102,48 @@ impl DialogButtonProps { mut self, on_cancel: impl Fn(&ClickEvent, &mut Window, &mut App) -> bool + 'static, ) -> Self { - self.on_cancel = Rc::new(on_cancel); + self.on_cancel = Some(Rc::new(on_cancel)); self } + /// Takes over every field `other` sets and keeps the rest. + pub(crate) fn merge(&mut self, other: Self) { + merge_field(&mut self.ok_text, other.ok_text); + merge_field(&mut self.ok_variant, other.ok_variant); + merge_field(&mut self.cancel_text, other.cancel_text); + merge_field(&mut self.cancel_variant, other.cancel_variant); + merge_field(&mut self.show_cancel, other.show_cancel); + merge_field(&mut self.on_ok, other.on_ok); + merge_field(&mut self.on_cancel, other.on_cancel); + merge_field(&mut self.on_close, other.on_close); + } + + /// Whether the default footer renders a Cancel button. Default is `false`. + pub(crate) fn is_cancel_shown(&self) -> bool { + self.show_cancel.unwrap_or(false) + } + + /// The confirm callback, defaulting to one that closes the dialog. + pub(crate) fn ok_handler(&self) -> OkHandler { + self.on_ok + .clone() + .unwrap_or_else(|| Rc::new(|_, _, _| true)) + } + + /// The cancel callback, defaulting to one that closes the dialog. + pub(crate) fn cancel_handler(&self) -> CancelHandler { + self.on_cancel + .clone() + .unwrap_or_else(|| Rc::new(|_, _, _| true)) + } + + /// The close callback, defaulting to one that does nothing. + pub(crate) fn close_handler(&self) -> CloseHandler { + self.on_close + .clone() + .unwrap_or_else(|| Rc::new(|_, _, _| {})) + } + pub(crate) fn render_ok(&self, _: &mut Window, _: &mut App) -> AnyElement { let ok_text = self .ok_text @@ -113,7 +154,7 @@ impl DialogButtonProps { anchor_key: "dialog-ok-anchor", button: Button::new("ok") .label(ok_text) - .with_variant(self.ok_variant), + .with_variant(self.ok_variant.unwrap_or(ButtonVariant::Primary)), action: Rc::new(Confirm { secondary: false }), } .into_any_element() @@ -129,7 +170,7 @@ impl DialogButtonProps { anchor_key: "dialog-cancel-anchor", button: Button::new("cancel") .label(cancel_text) - .with_variant(self.cancel_variant), + .with_variant(self.cancel_variant.unwrap_or_default()), action: Rc::new(Cancel), } .into_any_element() @@ -342,10 +383,15 @@ impl Dialog { } /// Set the button props of the dialog. + /// + /// This overrides only the fields `button_props` sets; the rest of the + /// dialog's button configuration is kept, so the call order does not + /// matter. pub fn button_props(mut self, button_props: DialogButtonProps) -> Self { - self.button_props = button_props; + self.button_props.merge(button_props); self } + pub(crate) fn with_base_alert_dialog(mut self, base: gpui_base::AlertDialog) -> Self { self.base = Some(BaseDialogRoot::AlertDialog(base)); self.props.overlay_closable = false; @@ -359,7 +405,7 @@ impl Dialog { mut self, on_close: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { - self.button_props.on_close = Rc::new(on_close); + self.button_props.on_close = Some(Rc::new(on_close)); self } @@ -511,9 +557,9 @@ impl RenderOnce for Dialog { let layer_ix = self.layer_ix; let selection_scope = self.selection_scope; - let on_close = self.button_props.on_close.clone(); - let on_ok = self.button_props.on_ok.clone(); - let on_cancel = self.button_props.on_cancel.clone(); + let on_close = self.button_props.close_handler(); + let on_ok = self.button_props.ok_handler(); + let on_cancel = self.button_props.cancel_handler(); let window_paddings = crate::window_border::window_paddings(window); let view_size = window.viewport_size() @@ -732,7 +778,7 @@ impl RenderOnce for Dialog { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use gpui::{AppContext as _, Bounds, Context, Render, TestAppContext, VisualTestContext, size}; @@ -748,7 +794,10 @@ mod tests { /// A window of `window_size` whose root renders the dialog layer, with /// motion reduced so the entrance animation settles on its first frame. - fn window(cx: &mut TestAppContext, window_size: gpui::Size) -> &mut VisualTestContext { + pub(crate) fn window( + cx: &mut TestAppContext, + window_size: gpui::Size, + ) -> &mut VisualTestContext { cx.update(|cx| { crate::init(cx); cx.set_reduce_motion(true); @@ -825,6 +874,22 @@ mod tests { ); } + /// `Dialog::button_props` overrides only the fields it sets. + #[gpui::test] + fn dialog_button_props_merge_with_what_the_dialog_already_carries(cx: &mut TestAppContext) { + let cx = window(cx, size(px(400.), px(300.))); + cx.update(|_, cx| { + let dialog = Dialog::new(cx) + .button_props(DialogButtonProps::default().cancel_text("Keep")) + .button_props(DialogButtonProps::default().ok_text("Delete")) + .button_props(DialogButtonProps::default().ok_variant(ButtonVariant::Danger)); + + assert_eq!(dialog.button_props.ok_text.as_deref(), Some("Delete")); + assert_eq!(dialog.button_props.cancel_text.as_deref(), Some("Keep")); + assert_eq!(dialog.button_props.ok_variant, Some(ButtonVariant::Danger)); + }); + } + /// Each stacked dialog steps down 16px; the deepest one must still end /// above the bottom margin. #[gpui::test] diff --git a/crates/story/src/stories/alert_dialog_story.rs b/crates/story/src/stories/alert_dialog_story.rs index 6b27136d24..7d74fc5a62 100644 --- a/crates/story/src/stories/alert_dialog_story.rs +++ b/crates/story/src/stories/alert_dialog_story.rs @@ -7,8 +7,8 @@ use gpui_kit::component::{ ActiveTheme, Icon, IconName, StyledExt, WindowExt as _, button::{Button, ButtonVariant, ButtonVariants}, dialog::{ - AlertDialog, DialogAction, DialogButtonProps, DialogClose, DialogDescription, DialogFooter, - DialogHeader, DialogTitle, + AlertDialog, DialogAction, DialogClose, DialogDescription, DialogFooter, DialogHeader, + DialogTitle, }, v_flex, }; @@ -100,8 +100,6 @@ impl Render for AlertDialogStory { .child(section("Imperative API").description("Open an alert directly from the window.").child( Button::new("confirm-alert").outline().label("Delete File").on_click(cx.listener( |_, _, window, cx| { - use gpui_kit::component::dialog::DialogButtonProps; - window.open_alert_dialog(cx, |alert, _, cx| { alert .icon(Icon::new(IconName::Info).text_color(cx.theme().danger)) @@ -110,13 +108,9 @@ impl Render for AlertDialogStory { "Are you sure you want to delete this file? \ This action cannot be undone.", ) - .button_props( - DialogButtonProps::default() - .ok_variant(ButtonVariant::Danger) - .ok_text("Delete") - .cancel_text("Cancel") - .show_cancel(true), - ) + .confirm() + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true @@ -285,7 +279,7 @@ impl Render for AlertDialogStory { "Please read this important notice \ carefully before proceeding.", ) - .button_props(DialogButtonProps::default().ok_text("Got It")) + .ok_text("Got It") .keyboard(false) }); }, @@ -313,7 +307,9 @@ impl Render for AlertDialogStory { .description( "Your changes are still syncing. The dialog remains open until syncing finishes.", ) - .button_props(DialogButtonProps::default().ok_text("Close").cancel_text("Wait").show_cancel(true)) + .confirm() + .ok_text("Close") + .cancel_text("Wait") .on_ok(|_, window, cx| { // Return false to prevent closing window.push_notification("Cannot close: Process still running", cx); diff --git a/skills/gpui-kit/references/usage.md b/skills/gpui-kit/references/usage.md index 99a6e3667a..8def75e061 100644 --- a/skills/gpui-kit/references/usage.md +++ b/skills/gpui-kit/references/usage.md @@ -230,21 +230,23 @@ object in the title and the result on the confirming button; see the Design Guides for the copy rules. ```rust -use gpui_kit::component::{button::ButtonVariant, dialog::DialogButtonProps}; +use gpui_kit::component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Remove “Roadmap”?") .description("Files on disk aren’t deleted.") - .button_props( - DialogButtonProps::default() - .ok_text("Remove") - .ok_variant(ButtonVariant::Danger) - .on_ok(|_, _, _| true), - ) + .confirm() + .ok_text("Remove") + .ok_variant(ButtonVariant::Danger) + .on_ok(|_, _, _| true) }); ``` +`button_props(DialogButtonProps)` takes the same properties as one value; it +overrides only the fields that value sets, so `confirm` and earlier callbacks +survive it. + ### Notification ```rust diff --git a/website/component/alert-dialog.md b/website/component/alert-dialog.md index 516b80c25f..be6f035f17 100644 --- a/website/component/alert-dialog.md +++ b/website/component/alert-dialog.md @@ -141,29 +141,46 @@ window.open_alert_dialog(cx, |alert, _, _| { }) ``` -### Custom Button Props +### Custom Buttons -Use `button_props` to customize button text and styles: +Set the button text and variant directly on the dialog: ```rust -use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete Account") .description("This will permanently delete your account and all associated data.") + .confirm() + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) + .cancel_text("Keep") + .on_ok(|_, window, cx| { + window.push_notification("Account deleted", cx); + true + }) +}) +``` + +`button_props` takes the same properties as one value, for a configuration you +want to build up or pass around. It overrides only the fields the value sets, +so everything the dialog already carries — the Cancel button `confirm` asked +for, a callback an earlier `on_ok` installed — survives, whatever the call +order: + +```rust +use gpui_kit::component::dialog::DialogButtonProps; + +window.open_alert_dialog(cx, move |alert, _, _| { + alert + .title("Delete Account") + .confirm() .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) - .cancel_text("Keep") - .show_cancel(true) ) - .on_ok(|_, window, cx| { - window.push_notification("Account deleted", cx); - true - }) }) ``` @@ -323,17 +340,12 @@ window.open_alert_dialog(cx, |alert, _, _| { Return `false` from `on_ok` or `on_cancel` callbacks to prevent the dialog from closing: ```rust -use gpui_kit::component::dialog::DialogButtonProps; - window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .description("A process is running. Click Continue to stop it or Cancel to keep waiting.") - .button_props( - DialogButtonProps::default() - .ok_text("Continue") - .show_cancel(true) - ) + .confirm() + .ok_text("Continue") .on_ok(|_, window, cx| { // Return false to prevent closing window.push_notification("Cannot close: Process still running", cx); @@ -373,7 +385,12 @@ window.open_alert_dialog(cx, |alert, _, _| { | `title(title)` | Set dialog title (imperative API) | | `description(desc)` | Set dialog description (imperative API) | | `icon(icon)` | Set dialog icon (imperative API) | -| `button_props(props)` | Set button properties (text, style, visibility) | +| `confirm()` | Show OK and Cancel buttons | +| `ok_text(text)` | Set OK button text, default "OK" | +| `ok_variant(variant)` | Set OK button variant, default `Primary` | +| `cancel_text(text)` | Set cancel button text, default "Cancel" | +| `cancel_variant(variant)`| Set cancel button variant | +| `button_props(props)` | Override the button properties the value sets, keep the rest | | `show_cancel(bool)` | Show/hide cancel button, default `false` | | `width(px)` | Set dialog width, default `420px` | | `overlay_closable(bool)` | Allow clicking overlay to close, default `false` | @@ -385,6 +402,9 @@ window.open_alert_dialog(cx, |alert, _, _| { ### DialogButtonProps +Every property is unset until a builder sets it, and an unset property keeps +whatever the dialog already carries. + | Method | Description | | ------------------------- | ---------------------------------------- | | `ok_text(text)` | Set OK button text, default "OK" | @@ -433,7 +453,7 @@ DialogClose::new().child( ### Delete Confirmation -Using imperative API with button props: +Using imperative API: ```rust Button::new("delete") @@ -444,12 +464,9 @@ Button::new("delete") alert .title("Delete File?") .description("This action cannot be undone.") - .button_props( - DialogButtonProps::default() - .ok_text("Delete") - .ok_variant(ButtonVariant::Danger) - .show_cancel(true) - ) + .confirm() + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) .on_ok(|_, window, cx| { // Perform delete operation window.push_notification("File deleted", cx); diff --git a/website/component/dialog.md b/website/component/dialog.md index 3c9687a0b3..d55c78b49b 100644 --- a/website/component/dialog.md +++ b/website/component/dialog.md @@ -138,6 +138,13 @@ window.open_dialog(cx, |dialog, _, _| { }) ``` +### Action Buttons + +A `Dialog` puts its own buttons in the [`footer`](#dialogfooter) and has them +dispatch `Confirm` or `Cancel`; `on_ok` and `on_cancel` decide what Enter and +Esc do. For a confirmation with default buttons, use +[AlertDialog](./alert-dialog.md). + ### Nested Dialogs ```rust diff --git a/website/zh-CN/component/alert-dialog.md b/website/zh-CN/component/alert-dialog.md index 949838a361..ff232f9136 100644 --- a/website/zh-CN/component/alert-dialog.md +++ b/website/zh-CN/component/alert-dialog.md @@ -142,27 +142,42 @@ window.open_alert_dialog(cx, |alert, _, _| { }) ``` -### 自定义按钮属性 +### 自定义按钮 + +直接在对话框上设置按钮文案和变体: ```rust -use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::button::ButtonVariant; window.open_alert_dialog(cx, |alert, _, _| { alert .title("Delete Account") .description("This will permanently delete your account and all associated data.") + .confirm() + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) + .cancel_text("Keep") + .on_ok(|_, window, cx| { + window.push_notification("Account deleted", cx); + true + }) +}) +``` + +`button_props` 把同样的属性打包成一个值,便于逐步拼装或在各处传递。它只覆盖该值中显式设置的字段,对话框上已有的配置——`confirm` 要求的取消按钮、此前 `on_ok` 注册的回调——都会保留,调用顺序也就不再重要: + +```rust +use gpui_kit::component::dialog::DialogButtonProps; + +window.open_alert_dialog(cx, move |alert, _, _| { + alert + .title("Delete Account") + .confirm() .button_props( DialogButtonProps::default() .ok_text("Delete") .ok_variant(ButtonVariant::Danger) - .cancel_text("Keep") - .show_cancel(true) ) - .on_ok(|_, window, cx| { - window.push_notification("Account deleted", cx); - true - }) }) ``` @@ -322,17 +337,12 @@ window.open_alert_dialog(cx, |alert, _, _| { 如果 `on_ok` 或 `on_cancel` 返回 `false`,对话框不会关闭: ```rust -use gpui_kit::component::dialog::DialogButtonProps; - window.open_alert_dialog(cx, |alert, _, _| { alert .title("Processing") .description("A process is running. Click Continue to stop it or Cancel to keep waiting.") - .button_props( - DialogButtonProps::default() - .ok_text("Continue") - .show_cancel(true) - ) + .confirm() + .ok_text("Continue") .on_ok(|_, window, cx| { window.push_notification("Cannot close: Process still running", cx); false @@ -369,7 +379,12 @@ window.open_alert_dialog(cx, |alert, _, _| { | `title(title)` | 设置标题,命令式 API | | `description(desc)` | 设置描述,命令式 API | | `icon(icon)` | 设置图标,命令式 API | -| `button_props(props)` | 设置按钮文本、样式和可见性 | +| `confirm()` | 同时显示确认与取消按钮 | +| `ok_text(text)` | 设置确认按钮文案,默认 `"OK"` | +| `ok_variant(variant)` | 设置确认按钮变体,默认 `Primary` | +| `cancel_text(text)` | 设置取消按钮文案,默认 `"Cancel"` | +| `cancel_variant(variant)` | 设置取消按钮变体 | +| `button_props(props)` | 只覆盖该值中显式设置的字段,其余保留 | | `show_cancel(bool)` | 显示或隐藏取消按钮,默认 `false` | | `width(px)` | 设置宽度,默认 `420px` | | `overlay_closable(bool)` | 是否允许点击遮罩关闭,默认 `false` | @@ -381,6 +396,8 @@ window.open_alert_dialog(cx, |alert, _, _| { ### DialogButtonProps +每个属性在被设置前都处于未设置状态,未设置的属性会保留对话框上已有的配置。 + | 方法 | 说明 | | ------------------------- | ---------------------------------------- | | `ok_text(text)` | 设置确认按钮文案,默认 `"OK"` | @@ -440,12 +457,9 @@ Button::new("delete") alert .title("Delete File?") .description("This action cannot be undone.") - .button_props( - DialogButtonProps::default() - .ok_text("Delete") - .ok_variant(ButtonVariant::Danger) - .show_cancel(true) - ) + .confirm() + .ok_text("Delete") + .ok_variant(ButtonVariant::Danger) .on_ok(|_, window, cx| { window.push_notification("File deleted", cx); true diff --git a/website/zh-CN/component/dialog.md b/website/zh-CN/component/dialog.md index 6e06f36f02..3e0bda6020 100644 --- a/website/zh-CN/component/dialog.md +++ b/website/zh-CN/component/dialog.md @@ -135,6 +135,10 @@ window.open_dialog(cx, |dialog, _, _| { }) ``` +### 操作按钮 + +`Dialog` 自己的按钮放在 [`footer`](#dialogfooter) 里并派发 `Confirm` / `Cancel`;`on_ok`、`on_cancel` 决定 Enter 与 Esc 的行为。需要默认按钮的确认框请用 [AlertDialog](./alert-dialog.md)。 + ### 嵌套对话框 ```rust