diff --git a/src/reactive/channel.rs b/src/reactive/channel.rs index bc2495e6c..b12d322cf 100644 --- a/src/reactive/channel.rs +++ b/src/reactive/channel.rs @@ -899,22 +899,6 @@ where self.data.force_send_inner(value, channel_id(&self.data)) } - /// Returns a [`Broadcaster`] that sends to this channel. - #[must_use] - pub fn create_broadcaster(&self) -> Broadcaster { - let mut data = self.data.synced.lock(); - data.senders += 1; - Broadcaster { - data: self.data.clone(), - } - } - - /// Returns this instance as a [`Broadcaster`] that sends to this channel. - #[must_use] - pub fn into_broadcaster(self) -> Broadcaster { - self.create_broadcaster() - } - /// Invokes `on_receive` each time a value is sent to this channel. /// /// This function assumes `on_receive` may block while waiting on another @@ -1036,6 +1020,24 @@ where } } +impl BroadcastChannel { + /// Returns a [`Broadcaster`] that sends to this channel. + #[must_use] + pub fn create_broadcaster(&self) -> Broadcaster { + let mut data = self.data.synced.lock(); + data.senders += 1; + Broadcaster { + data: self.data.clone(), + } + } + + /// Returns this instance as a [`Broadcaster`] that sends to this channel. + #[must_use] + pub fn into_broadcaster(self) -> Broadcaster { + self.create_broadcaster() + } +} + impl Clone for BroadcastChannel { fn clone(&self) -> Self { let mut data = self.data.synced.lock(); @@ -1071,6 +1073,7 @@ impl Drop for BroadcastChannel { } /// Sends values to a [`BroadcastChannel`]. +#[derive(Debug)] pub struct Broadcaster { data: Arc>>, } diff --git a/src/reactive/value.rs b/src/reactive/value.rs index 81bb0fff2..e69c2f91a 100644 --- a/src/reactive/value.rs +++ b/src/reactive/value.rs @@ -26,7 +26,7 @@ use crate::reactive::{ }; use crate::utils::WithClone; use crate::widget::{ - MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance, WidgetList, + MakeWidget, MakeWidgetWithTag, Notify, OnceCallback, WidgetId, WidgetInstance, WidgetList, }; use crate::widgets::checkbox::CheckboxState; use crate::widgets::{Checkbox, Radio, Select, Space, Switcher}; @@ -305,6 +305,29 @@ pub trait Source { }) } + /// Notifies `notify` with a clone of the current contents each time this + /// source's contents are updated. + fn for_each_notify(&self, notify: impl Into>) -> CallbackHandle + where + T: Unpin + Clone + Send + 'static, + { + let mut notify = notify.into(); + self.for_each_cloned(move |value| notify.notify(value)) + } + + /// Notifies `notify` with a clone of the current contents each time this + /// source's contents are updated, disconnecting the callback if the target + /// is disconnected. + fn for_each_try_notify(&self, notify: impl Into>) -> CallbackHandle + where + T: Unpin + Clone + Send + 'static, + { + let mut notify = notify.into(); + self.for_each_cloned_try(move |value| { + notify.try_notify(value).map_err(|_| CallbackDisconnected) + }) + } + /// Returns a new dynamic that contains the updated contents of this dynamic /// at most once every `period`. #[must_use] diff --git a/src/widget.rs b/src/widget.rs index 49fb9974e..5fec15342 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -21,6 +21,7 @@ use parking_lot::{Mutex, MutexGuard}; use unic_langid::LanguageIdentifier; use crate::app::Run; +use crate::channel::{BroadcastChannel, Broadcaster, Sender}; use crate::context::sealed::Trackable as _; use crate::context::{ AsEventContext, EventContext, GraphicsContext, LayoutContext, ManageWidget, WidgetContext, @@ -1832,6 +1833,187 @@ where } } +/// A target for notifications. +/// +/// Cushy provides several ways to create reactivity. This type allows `From` +/// conversion from all types that can act as a receiver of values of `T`. +#[derive(Debug)] +#[non_exhaustive] +pub enum Notify { + /// A callback/function that is invoked for each `T`. + Callback(Callback), + /// A callback/function that is invoked for each `T` that can be cloned and + /// used in multiple locations. + SharedCallback(SharedCallback), + /// A channel that is sent each `T`. + Sender(Sender), + /// A broadcast channel that is sent each `T`. + Broadcaster(Broadcaster), + /// A dynamic that is updated with each `T`. + Dynamic(Dynamic), +} + +impl Notify +where + T: Unpin + Clone + Send + 'static, +{ + /// Notifies the target of the new `value`. + pub fn notify(&mut self, value: T) { + match self { + Notify::Callback(callback) => callback.invoke(value), + Notify::SharedCallback(callback) => callback.invoke(value), + Notify::Sender(sender) => { + let _ = sender.force_send(value); + } + Notify::Broadcaster(broadcaster) => { + let _ = broadcaster.force_send(value); + } + Notify::Dynamic(dynamic) => { + *dynamic.lock() = value; + } + } + } + + /// Notifies the target of the new `value`, returning an error if the target + /// is no longer reachable. + pub fn try_notify(&mut self, value: T) -> Result<(), T> { + match self { + Notify::Callback(callback) => callback.invoke(value), + Notify::SharedCallback(callback) => callback.invoke(value), + Notify::Sender(sender) => sender.force_send(value).map(|_| ())?, + Notify::Broadcaster(broadcaster) => broadcaster.force_send(value).map(|_| ())?, + Notify::Dynamic(dynamic) => { + *dynamic.lock() = value; + } + } + + Ok(()) + } +} + +impl From> for Notify { + fn from(value: Callback) -> Self { + Self::Callback(value) + } +} + +impl From> for Notify { + fn from(value: SharedCallback) -> Self { + Self::SharedCallback(value) + } +} + +impl From> for Notify { + fn from(value: Dynamic) -> Self { + Self::Dynamic(value) + } +} + +impl From> for Notify { + fn from(value: Sender) -> Self { + Self::Sender(value) + } +} + +impl From> for Notify { + fn from(value: Broadcaster) -> Self { + Self::Broadcaster(value) + } +} + +impl From> for Notify { + fn from(value: BroadcastChannel) -> Self { + Self::Broadcaster(value.into_broadcaster()) + } +} + +impl From for Notify +where + F: FnMut(T) + Send + 'static, +{ + fn from(func: F) -> Self { + Self::from(Callback::new(func)) + } +} + +/// A target for notifications. +/// +/// Cushy provides several ways to create reactivity. This type allows `From` +/// conversion from all types that can act as a receiver of values of `T`. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum SharedNotify { + /// A callback/function that is invoked for each `T` that can be cloned and + /// used in multiple locations. + SharedCallback(SharedCallback), + /// A channel that is sent each `T`. + Sender(Sender), + /// A broadcast channel that is sent each `T`. + Broadcaster(Broadcaster), + /// A dynamic that is updated with each `T`. + Dynamic(Dynamic), +} + +impl SharedNotify +where + T: Unpin + Clone + Send + 'static, +{ + /// Notifies the target of the new `value`. + pub fn notify(&mut self, value: T) { + match self { + Self::SharedCallback(callback) => callback.invoke(value), + Self::Sender(sender) => { + let _ = sender.force_send(value); + } + Self::Broadcaster(broadcaster) => { + let _ = broadcaster.force_send(value); + } + Self::Dynamic(dynamic) => { + *dynamic.lock() = value; + } + } + } +} + +impl From> for SharedNotify { + fn from(value: SharedCallback) -> Self { + Self::SharedCallback(value) + } +} + +impl From> for SharedNotify { + fn from(value: Dynamic) -> Self { + Self::Dynamic(value) + } +} + +impl From> for SharedNotify { + fn from(value: Sender) -> Self { + Self::Sender(value) + } +} + +impl From> for SharedNotify { + fn from(value: Broadcaster) -> Self { + Self::Broadcaster(value) + } +} + +impl From> for SharedNotify { + fn from(value: BroadcastChannel) -> Self { + Self::Broadcaster(value.into_broadcaster()) + } +} + +impl From for SharedNotify +where + F: FnMut(T) + Send + 'static, +{ + fn from(func: F) -> Self { + Self::from(SharedCallback::new(func)) + } +} + /// A [`Callback`] that can be cloned. /// /// Only one thread can be invoking a shared callback at any given time. diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 6490085b0..1f6d611e0 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -22,7 +22,7 @@ use crate::styles::components::{ use crate::styles::{ColorExt, Styles}; use crate::value::{Destination, Dynamic, IntoValue, Source, Value}; use crate::widget::{ - Callback, EventHandling, MakeWidget, SharedCallback, Widget, WidgetRef, HANDLED, + EventHandling, MakeWidget, Notify, SharedCallback, Widget, WidgetRef, HANDLED, }; use crate::window::{DeviceId, WindowLocal}; use crate::FitMeasuredSize; @@ -33,7 +33,7 @@ pub struct Button { /// The label to display on the button. pub content: WidgetRef, /// The callback that is invoked when the button is clicked. - pub on_click: Option>>, + pub on_click: Option>>, /// The kind of button to draw. pub kind: Value, focusable: bool, @@ -159,11 +159,17 @@ impl Button { /// /// This callback will be invoked each time the button is clicked. #[must_use] - pub fn on_click(mut self, callback: F) -> Self + pub fn on_click(self, callback: F) -> Self where F: FnMut(Option) + Send + 'static, { - self.on_click = Some(Callback::new(callback)); + self.on_click_notify(callback) + } + + /// Sets `notify` to receive each click of this button, and returns self. + #[must_use] + pub fn on_click_notify(mut self, notify: impl Into>>) -> Self { + self.on_click = Some(notify.into()); self } @@ -177,7 +183,7 @@ impl Button { fn invoke_on_click(&mut self, button: Option, context: &WidgetContext<'_>) { if context.enabled() { if let Some(on_click) = self.on_click.as_mut() { - on_click.invoke(button); + on_click.notify(button); } } } diff --git a/src/widgets/menu.rs b/src/widgets/menu.rs index 657ece3b2..f19c5c5c3 100644 --- a/src/widgets/menu.rs +++ b/src/widgets/menu.rs @@ -24,7 +24,7 @@ use crate::styles::components::{ use crate::styles::Styles; use crate::value::{Dynamic, IntoValue, Source, Value}; use crate::widget::{ - Callback, EventHandling, MakeWidget, MakeWidgetWithTag, SharedCallback, Widget, WidgetId, + Callback, EventHandling, MakeWidget, MakeWidgetWithTag, SharedNotify, Widget, WidgetId, WidgetInstance, WidgetRef, WidgetTag, HANDLED, }; use crate::ConstraintLimit; @@ -75,9 +75,16 @@ where where F: FnMut(T) + Send + 'static, { + self.on_selected_notify(selected) + } + + /// Sets the selected handler to `selected`, causing it to be notified when + /// an item is chosen. + #[must_use] + pub fn on_selected_notify(self, selected: impl Into>) -> Menu { Menu { items: self.items, - on_click: MenuHandler(SharedCallback::new(selected)), + on_click: MenuHandler(selected.into()), } } } @@ -103,7 +110,7 @@ where impl Menu where - T: Debug + Send + Clone + 'static, + T: Unpin + Debug + Send + Clone + 'static, { /// Presents this menu in `overlay`, returning an [`Overlayable`] that can /// be positioned relative or absolutely within `overlay`. @@ -352,7 +359,7 @@ impl sealed::MenuItemContentsSealed for WidgetInstance { impl sealed::SubmenuFactory for Menu where - T: Clone + std::fmt::Debug + Send + Sync + 'static, + T: Unpin + Clone + std::fmt::Debug + Send + Sync + 'static, { fn overlay_submenu_in<'overlay>( &self, @@ -371,7 +378,7 @@ where #[must_use] pub fn submenu(mut self, submenu: Menu) -> Self where - U: Clone + Debug + Send + Sync + 'static, + U: Unpin + Clone + Debug + Send + Sync + 'static, { self.submenu = Some(Arc::new(submenu)); self @@ -449,7 +456,7 @@ where /// A handler for a selected [`MenuItem`]. #[derive(Debug, Clone)] -pub struct MenuHandler(SharedCallback); +pub struct MenuHandler(SharedNotify); #[derive(Debug)] struct OpenMenu { @@ -536,7 +543,7 @@ impl OpenMenu { impl Widget for OpenMenu where - T: Clone + Debug + Send + 'static, + T: Unpin + Clone + Debug + Send + 'static, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { self.update_visual_state(&mut context.as_event_context()); @@ -762,7 +769,7 @@ where let ItemKind::Item(item) = &self.items[index].item else { return; }; - self.on_click.0.invoke(item.value.clone()); + self.on_click.0.notify(item.value.clone()); let mut shared = self.shared.lock(); for handle in shared.open_menus.drain() { handle.dismiss(); diff --git a/src/window.rs b/src/window.rs index ad72f0969..516a567fc 100644 --- a/src/window.rs +++ b/src/window.rs @@ -61,7 +61,7 @@ use crate::value::{ Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Tracked, Value, }; use crate::widget::{ - Callback, EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, SharedCallback, + EventHandling, MakeWidget, MountedWidget, Notify, OnceCallback, RootBehavior, SharedCallback, WidgetId, WidgetInstance, HANDLED, IGNORED, }; use crate::widgets::shortcuts::{ShortcutKey, ShortcutMap}; @@ -574,7 +574,7 @@ where enabled_buttons: Option>, fullscreen: Option>>, shortcuts: Value, - on_file_drop: Option>, + on_file_drop: Option>, } impl Default for Window @@ -1027,11 +1027,16 @@ where } /// Invokes `on_file_drop` when a file is hovered or dropped on this window. - pub fn on_file_drop(mut self, on_file_drop: Function) -> Self + pub fn on_file_drop(self, on_file_drop: Function) -> Self where Function: FnMut(FileDrop) + Send + 'static, { - self.on_file_drop = Some(Callback::new(on_file_drop)); + self.on_file_drop_notify(on_file_drop) + } + + /// Notifies `on_file_drop` when a file is hovered or dropped on this window. + pub fn on_file_drop_notify(mut self, on_file_drop: impl Into>) -> Self { + self.on_file_drop = Some(on_file_drop.into()); self } @@ -1254,6 +1259,7 @@ where } /// A file drop event for a window. +#[derive(Clone, Debug)] pub struct FileDrop { /// The handle to the window the file drop event is for. pub window: WindowHandle, @@ -1262,6 +1268,7 @@ pub struct FileDrop { } /// A drop event. +#[derive(Clone, Debug)] pub enum DropEvent { /// The payload is being hovered over the container. Hover(T), @@ -1376,7 +1383,7 @@ struct OpenWindow { fullscreen: Tracked>>, modifiers: Dynamic, shortcuts: Value, - on_file_drop: Option>, + on_file_drop: Option>, disabled_resize_automatically: bool, } @@ -2567,7 +2574,7 @@ where window: &kludgine::app::Window<'_, WindowCommand>, ) { if let Some(on_file_drop) = &mut self.on_file_drop { - on_file_drop.invoke(FileDrop { + on_file_drop.notify(FileDrop { window: WindowHandle::new(window.handle(), self.redraw_status.clone()), drop, }); @@ -3074,7 +3081,7 @@ pub(crate) mod sealed { use crate::fonts::FontCollection; use crate::styles::{FontFamilyList, ThemePair}; use crate::value::{Dynamic, Value}; - use crate::widget::{Callback, OnceCallback, SharedCallback}; + use crate::widget::{Notify, OnceCallback, SharedCallback}; use crate::widgets::shortcuts::ShortcutMap; use crate::window::{FileDrop, PendingWindow, ThemeMode, WindowAttributes, WindowHandle}; use crate::{App, MaybeLocalized}; @@ -3129,7 +3136,7 @@ pub(crate) mod sealed { pub enabled_buttons: Value, pub fullscreen: Value>, pub shortcuts: Value, - pub on_file_drop: Option>, + pub on_file_drop: Option>, } pub struct WindowExecute(Box);