Skip to content

Commit

Permalink
Added Notify/SharedNotify to unify message passing
Browse files Browse the repository at this point in the history
There aren't many places where Cushy outputs a signal and doesn't expect
a response. Buttons and menus seem to be the only places where values
are sent and not received. Many of the value based widgets often need to
store and read the values, leading to channel integration not really
being useful.

So in the end, it seems like channel support in Cushy really is focused
around solving certain data flow problems easier by leveraging a runtime
provided by Cushy.
  • Loading branch information
ecton committed Jan 21, 2025
1 parent ac1b0c3 commit 525b5bb
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 38 deletions.
35 changes: 19 additions & 16 deletions src/reactive/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
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<T> {
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
Expand Down Expand Up @@ -1036,6 +1020,24 @@ where
}
}

impl<T> BroadcastChannel<T> {
/// Returns a [`Broadcaster`] that sends to this channel.
#[must_use]
pub fn create_broadcaster(&self) -> Broadcaster<T> {
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<T> {
self.create_broadcaster()
}
}

impl<T> Clone for BroadcastChannel<T> {
fn clone(&self) -> Self {
let mut data = self.data.synced.lock();
Expand Down Expand Up @@ -1071,6 +1073,7 @@ impl<T> Drop for BroadcastChannel<T> {
}

/// Sends values to a [`BroadcastChannel`].
#[derive(Debug)]
pub struct Broadcaster<T> {
data: Arc<ChannelData<T, MultipleCallbacks<T>>>,
}
Expand Down
25 changes: 24 additions & 1 deletion src/reactive/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -305,6 +305,29 @@ pub trait Source<T> {
})
}

/// 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<Notify<T>>) -> 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<Notify<T>>) -> 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]
Expand Down
182 changes: 182 additions & 0 deletions src/widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<T> {
/// A callback/function that is invoked for each `T`.
Callback(Callback<T>),
/// A callback/function that is invoked for each `T` that can be cloned and
/// used in multiple locations.
SharedCallback(SharedCallback<T>),
/// A channel that is sent each `T`.
Sender(Sender<T>),
/// A broadcast channel that is sent each `T`.
Broadcaster(Broadcaster<T>),
/// A dynamic that is updated with each `T`.
Dynamic(Dynamic<T>),
}

impl<T> Notify<T>
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<T> From<Callback<T>> for Notify<T> {
fn from(value: Callback<T>) -> Self {
Self::Callback(value)
}
}

impl<T> From<SharedCallback<T>> for Notify<T> {
fn from(value: SharedCallback<T>) -> Self {
Self::SharedCallback(value)
}
}

impl<T> From<Dynamic<T>> for Notify<T> {
fn from(value: Dynamic<T>) -> Self {
Self::Dynamic(value)
}
}

impl<T> From<Sender<T>> for Notify<T> {
fn from(value: Sender<T>) -> Self {
Self::Sender(value)
}
}

impl<T> From<Broadcaster<T>> for Notify<T> {
fn from(value: Broadcaster<T>) -> Self {
Self::Broadcaster(value)
}
}

impl<T> From<BroadcastChannel<T>> for Notify<T> {
fn from(value: BroadcastChannel<T>) -> Self {
Self::Broadcaster(value.into_broadcaster())
}
}

impl<T, F> From<F> for Notify<T>
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<T> {
/// A callback/function that is invoked for each `T` that can be cloned and
/// used in multiple locations.
SharedCallback(SharedCallback<T>),
/// A channel that is sent each `T`.
Sender(Sender<T>),
/// A broadcast channel that is sent each `T`.
Broadcaster(Broadcaster<T>),
/// A dynamic that is updated with each `T`.
Dynamic(Dynamic<T>),
}

impl<T> SharedNotify<T>
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<T> From<SharedCallback<T>> for SharedNotify<T> {
fn from(value: SharedCallback<T>) -> Self {
Self::SharedCallback(value)
}
}

impl<T> From<Dynamic<T>> for SharedNotify<T> {
fn from(value: Dynamic<T>) -> Self {
Self::Dynamic(value)
}
}

impl<T> From<Sender<T>> for SharedNotify<T> {
fn from(value: Sender<T>) -> Self {
Self::Sender(value)
}
}

impl<T> From<Broadcaster<T>> for SharedNotify<T> {
fn from(value: Broadcaster<T>) -> Self {
Self::Broadcaster(value)
}
}

impl<T> From<BroadcastChannel<T>> for SharedNotify<T> {
fn from(value: BroadcastChannel<T>) -> Self {
Self::Broadcaster(value.into_broadcaster())
}
}

impl<T, F> From<F> for SharedNotify<T>
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.
Expand Down
16 changes: 11 additions & 5 deletions src/widgets/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Callback<Option<ButtonClick>>>,
pub on_click: Option<Notify<Option<ButtonClick>>>,
/// The kind of button to draw.
pub kind: Value<ButtonKind>,
focusable: bool,
Expand Down Expand Up @@ -159,11 +159,17 @@ impl Button {
///
/// This callback will be invoked each time the button is clicked.
#[must_use]
pub fn on_click<F>(mut self, callback: F) -> Self
pub fn on_click<F>(self, callback: F) -> Self
where
F: FnMut(Option<ButtonClick>) + 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<Notify<Option<ButtonClick>>>) -> Self {
self.on_click = Some(notify.into());
self
}

Expand All @@ -177,7 +183,7 @@ impl Button {
fn invoke_on_click(&mut self, button: Option<ButtonClick>, context: &WidgetContext<'_>) {
if context.enabled() {
if let Some(on_click) = self.on_click.as_mut() {
on_click.invoke(button);
on_click.notify(button);
}
}
}
Expand Down
Loading

0 comments on commit 525b5bb

Please sign in to comment.