From 444fbbe4ed09559ecae3a1004308e64ff4bc20dc Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 12 Sep 2024 14:49:37 -0700 Subject: [PATCH] Modal DialogBuilder --- Cargo.lock | 4 +- examples/modal.rs | 14 +--- src/widgets/layers.rs | 169 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4dc8accb..67bef7a2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,9 +146,9 @@ checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" [[package]] name = "arboard" -version = "3.4.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" +checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", "core-graphics", diff --git a/examples/modal.rs b/examples/modal.rs index 59c912df3..5af905feb 100644 --- a/examples/modal.rs +++ b/examples/modal.rs @@ -9,22 +9,10 @@ fn main() -> cushy::Result { .into_button() .on_click({ let modal = modal.clone(); - move |_| { - modal.present(dialog(&modal)); - } + move |_| modal.message("This is a modal", "Dismiss") }) .align_top() .and(modal) .into_layers() .run() } - -fn dialog(modal: &Modal) -> impl MakeWidget { - let modal = modal.clone(); - "This is a modal" - .and("Dismiss".into_button().on_click(move |_| { - modal.dismiss(); - })) - .into_rows() - .contain() -} diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 54a60bb0d..1dcb207f5 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -1,6 +1,7 @@ //! Widgets that stack in the Z-direction. use std::fmt::{self, Debug}; +use std::marker::PhantomData; use std::time::Duration; use alot::{LotId, OrderedLots}; @@ -908,6 +909,19 @@ impl Modal { self.modal.set(Some(contents.make_widget())); } + /// Presents a modal dialog containing `message` with a default button that + /// dismisses the dialog. + pub fn message(&self, message: impl MakeWidget, button_caption: impl MakeWidget) { + self.build_dialog(message) + .with_default_button(button_caption, || {}) + .show(); + } + + /// Returns a builder for a modal dialog that displays `message`. + pub fn build_dialog(&self, message: impl MakeWidget) -> DialogBuilder { + DialogBuilder::new(self, message) + } + /// Dismisses the modal session. pub fn dismiss(&self) { self.modal.set(None); @@ -918,6 +932,18 @@ impl Modal { pub fn visible(&self) -> bool { self.modal.map_ref(Option::is_some) } + + /// Returns a function that dismisses the modal when invoked. + /// + /// The input to the function is ignored. This function takes a single + /// argument so that it is compatible with widgets that use a [`Callback`] + /// for their events. + pub fn dismiss_callback(&self) -> impl FnMut(T) + Send + 'static { + let modal = self.clone(); + move |_| { + modal.dismiss(); + } + } } impl MakeWidget for Modal { @@ -983,3 +1009,146 @@ impl Widget for ModalLayer { self.presented.is_some() } } + +/// A marker type indicating a special [`DialogBuilder`] button type is not +/// present. +pub enum No {} + +/// A marker type indicating a special [`DialogBuilder`] button type is present. +pub enum Yes {} + +/// A modal dialog builder. +#[must_use = "DialogBuilder::show must be called for the dialog to be shown"] +pub struct DialogBuilder { + modal: Modal, + message: WidgetInstance, + buttons: WidgetList, + _state: PhantomData<(HasDefault, HasCancel)>, +} + +impl DialogBuilder { + fn new(modal: &Modal, message: impl MakeWidget) -> Self { + Self { + modal: modal.clone(), + message: message.make_widget(), + buttons: WidgetList::new(), + _state: PhantomData, + } + } +} + +impl DialogBuilder { + /// Adds a button with `caption` that invokes `on_click` when activated. + /// Returns self. + pub fn with_button( + mut self, + caption: impl MakeWidget, + on_click: impl FnOnce() + Send + 'static, + ) -> Self { + self.push_button(caption, on_click); + self + } + + /// Pushes a button with `caption` that invokes `on_click` when activated. + pub fn push_button( + &mut self, + caption: impl MakeWidget, + on_click: impl FnOnce() + Send + 'static, + ) { + self.inner_push_button(caption, DialogButtonKind::Plain, on_click); + } + + fn inner_push_button( + &mut self, + caption: impl MakeWidget, + kind: DialogButtonKind, + on_click: impl FnOnce() + Send + 'static, + ) { + let mut on_click = Some(on_click); + let modal = self.modal.clone(); + let mut button = caption + .into_button() + .on_click(move |_| { + let Some(on_click) = on_click.take() else { + return; + }; + modal.dismiss(); + on_click(); + }) + .make_widget(); + match kind { + DialogButtonKind::Plain => {} + DialogButtonKind::Default => button = button.into_default(), + DialogButtonKind::Cancel => button = button.into_escape(), + } + self.buttons.push(button.fit_horizontally().make_widget()); + } + + /// Shows the modal dialog. + pub fn show(mut self) { + if self.buttons.is_empty() { + self.inner_push_button("OK", DialogButtonKind::Default, || {}); + } + self.modal.present( + self.message + .and(self.buttons.into_columns().centered()) + .into_rows() + .contain(), + ); + } +} + +impl DialogBuilder { + /// Adds a default button with `caption` that invokes `on_click` when + /// activated. + pub fn with_default_button( + mut self, + caption: impl MakeWidget, + on_click: impl FnOnce() + Send + 'static, + ) -> DialogBuilder { + self.inner_push_button(caption, DialogButtonKind::Default, on_click); + let Self { + modal, + message, + buttons, + _state, + } = self; + DialogBuilder { + modal, + message, + buttons, + _state: PhantomData, + } + } +} + +impl DialogBuilder { + /// Adds a cancel button with `caption` that invokes `on_click` when + /// activated. + pub fn with_cancel_button( + mut self, + caption: impl MakeWidget, + on_click: impl FnOnce() + Send + 'static, + ) -> DialogBuilder { + self.inner_push_button(caption, DialogButtonKind::Cancel, on_click); + let Self { + modal, + message, + buttons, + _state, + } = self; + DialogBuilder { + modal, + message, + buttons, + _state: PhantomData, + } + } +} + +#[derive(Clone, Copy)] +enum DialogButtonKind { + Plain, + Default, + Cancel, +}