Modal DialogBuilder

This commit is contained in:
Jonathan Johnson 2024-09-12 14:49:37 -07:00
parent 2f387c9845
commit 444fbbe4ed
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
3 changed files with 172 additions and 15 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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()
}

View file

@ -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<T>(&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<HasDefault = No, HasCancel = No> {
modal: Modal,
message: WidgetInstance,
buttons: WidgetList,
_state: PhantomData<(HasDefault, HasCancel)>,
}
impl DialogBuilder<No, No> {
fn new(modal: &Modal, message: impl MakeWidget) -> Self {
Self {
modal: modal.clone(),
message: message.make_widget(),
buttons: WidgetList::new(),
_state: PhantomData,
}
}
}
impl<HasDefault, HasCancel> DialogBuilder<HasDefault, HasCancel> {
/// 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<HasCancel> DialogBuilder<No, HasCancel> {
/// 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<Yes, HasCancel> {
self.inner_push_button(caption, DialogButtonKind::Default, on_click);
let Self {
modal,
message,
buttons,
_state,
} = self;
DialogBuilder {
modal,
message,
buttons,
_state: PhantomData,
}
}
}
impl<HasDefault> DialogBuilder<HasDefault, No> {
/// 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<HasDefault, Yes> {
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,
}