freya_radio/hooks/
use_radio.rs

1use std::{
2    cell::RefCell,
3    collections::HashMap,
4    hash::Hash,
5    ops::{
6        Deref,
7        DerefMut,
8    },
9    rc::Rc,
10};
11
12use freya_core::{
13    integration::FxHashSet,
14    prelude::*,
15};
16
17#[cfg(feature = "tracing")]
18pub trait RadioChannel<T>: 'static + PartialEq + Eq + Clone + Hash + std::fmt::Debug + Ord {
19    fn derive_channel(self, _radio: &T) -> Vec<Self> {
20        vec![self]
21    }
22}
23
24/// Defines a channel for radio communication.
25/// Channels are used to subscribe to specific changes in the global state.
26/// Each channel must implement this trait to be used with [`RadioStation`] and [`Radio`].
27///
28/// Channels allow fine-grained control over which components re-render when the state changes.
29/// Components only re-render when a channel they are subscribed to is notified.
30///
31/// # Example
32///
33/// ```rust, no_run
34/// # use freya::radio::*;
35///
36/// # struct Data;
37///
38/// #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
39/// pub enum DataChannel {
40///     ListCreation,
41///     SpecificListItemUpdate(usize),
42/// }
43///
44/// impl RadioChannel<Data> for DataChannel {}
45/// ```
46#[cfg(not(feature = "tracing"))]
47pub trait RadioChannel<T>: 'static + PartialEq + Eq + Clone + Hash {
48    /// Derive additional channels based on the current state value.
49    /// This allows a single write operation to notify multiple channels.
50    ///
51    /// By default, returns a vector containing only `self`.
52    ///
53    /// # Example
54    ///
55    /// ```rust, no_run
56    /// # use freya::radio::*;
57    ///
58    /// # struct Data;
59    ///
60    /// #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
61    /// pub enum DataChannel {
62    ///     All,
63    ///     Specific(usize),
64    /// }
65    ///
66    /// impl RadioChannel<Data> for DataChannel {
67    ///     fn derive_channel(self, _data: &Data) -> Vec<Self> {
68    ///         match self {
69    ///             DataChannel::All => vec![DataChannel::All],
70    ///             DataChannel::Specific(id) => vec![DataChannel::All, DataChannel::Specific(id)],
71    ///         }
72    ///     }
73    /// }
74    /// ```
75    fn derive_channel(self, _radio: &T) -> Vec<Self> {
76        vec![self]
77    }
78}
79
80/// The central hub for global state management in Freya applications.
81/// A `RadioStation` holds the global state value and manages subscriptions to different channels.
82/// Components can subscribe to specific channels to receive notifications when the state changes.
83///
84/// RadioStations can be shared across multiple windows or components using [`use_share_radio`].
85///
86/// # Examples
87///
88/// ## Basic usage
89///
90/// ```rust, no_run
91/// # use freya::prelude::*;
92/// # use freya::radio::*;
93///
94/// #[derive(Default)]
95/// struct AppState {
96///     count: i32,
97/// }
98///
99/// #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash)]
100/// enum AppChannel {
101///     Count,
102/// }
103///
104/// impl RadioChannel<AppState> for AppChannel {}
105///
106/// fn app() -> impl IntoElement {
107///     // Create a radio station (scoped to this component tree)
108///     use_init_radio_station::<AppState, AppChannel>(AppState::default);
109///
110///     let mut radio = use_radio(AppChannel::Count);
111///
112///     rect()
113///         .child(label().text(format!("Count: {}", radio.read().count)))
114///         .child(
115///             Button::new()
116///                 .on_press(move |_| radio.write().count += 1)
117///                 .child("Increment"),
118///         )
119/// }
120/// ```
121///
122/// ## Global radio station for multi-window apps
123///
124/// ```rust, ignore
125/// # use freya::prelude::*;
126/// # use freya::radio::*;
127///
128/// let radio_station = RadioStation::create_global(AppState::default);
129///
130/// launch(
131///     LaunchConfig::new()
132///         .with_window(WindowConfig::new(Window1 { radio_station }))
133///         .with_window(WindowConfig::new(Window2 { radio_station })),
134/// );
135/// ```
136pub struct RadioStation<Value, Channel>
137where
138    Channel: RadioChannel<Value>,
139    Value: 'static,
140{
141    pub(crate) value: State<Value>,
142    listeners: State<HashMap<Channel, Rc<RefCell<FxHashSet<ReactiveContext>>>>>,
143}
144
145impl<Value, Channel> Clone for RadioStation<Value, Channel>
146where
147    Channel: RadioChannel<Value>,
148{
149    fn clone(&self) -> Self {
150        *self
151    }
152}
153
154impl<Value, Channel> Copy for RadioStation<Value, Channel> where Channel: RadioChannel<Value> {}
155
156impl<Value, Channel> RadioStation<Value, Channel>
157where
158    Channel: RadioChannel<Value>,
159{
160    pub(crate) fn create(init_value: Value) -> Self {
161        RadioStation {
162            value: State::create(init_value),
163            listeners: State::create(HashMap::default()),
164        }
165    }
166
167    /// Create a global `RadioStation` that lives for the entire application lifetime.
168    /// This is useful for sharing state across multiple windows.
169    ///
170    /// You would usually want to call this in your `main` function, not anywhere else.
171    ///
172    /// # Example
173    ///
174    /// ```rust, ignore
175    /// # use freya::prelude::*;
176    /// # use freya::radio::*;
177    ///
178    /// let radio_station = RadioStation::create_global(AppState::default);
179    ///
180    /// launch(
181    ///     LaunchConfig::new()
182    ///         .with_window(WindowConfig::new(Window1 { radio_station }))
183    ///         .with_window(WindowConfig::new(Window2 { radio_station })),
184    /// );
185    /// ```
186    pub fn create_global(init_value: Value) -> Self {
187        RadioStation {
188            value: State::create_global(init_value),
189            listeners: State::create_global(HashMap::default()),
190        }
191    }
192
193    pub(crate) fn is_listening(
194        &self,
195        channel: &Channel,
196        reactive_context: &ReactiveContext,
197    ) -> bool {
198        let listeners = self.listeners.peek();
199        listeners
200            .get(channel)
201            .map(|contexts| contexts.borrow().contains(reactive_context))
202            .unwrap_or_default()
203    }
204
205    pub(crate) fn listen(&self, channel: Channel, mut reactive_context: ReactiveContext) {
206        let mut listeners = self.listeners.write_unchecked();
207        let listeners = listeners.entry(channel).or_default();
208        reactive_context.subscribe(listeners);
209    }
210
211    pub(crate) fn notify_listeners(&self, channel: &Channel) {
212        let listeners = self.listeners.write_unchecked();
213
214        #[cfg(feature = "tracing")]
215        tracing::info!("Notifying {channel:?}");
216
217        for (listener_channel, listeners) in listeners.iter() {
218            if listener_channel == channel {
219                for reactive_context in listeners.borrow().iter() {
220                    reactive_context.notify();
221                }
222            }
223        }
224    }
225
226    /// Read the current state value and subscribe to all channel changes.
227    /// Any component calling this will re-render when any channel is notified.
228    ///
229    /// # Example
230    ///
231    /// ```rust, ignore
232    /// # use freya::radio::*;
233    /// let value = radio_station.read();
234    /// ```
235    pub fn read(&'_ self) -> ReadRef<'_, Value> {
236        self.value.read()
237    }
238
239    pub fn peek_unchecked(&self) -> ReadRef<'static, Value> {
240        self.value.peek()
241    }
242
243    /// Read the current state value without subscribing to changes.
244    /// Components using this will not re-render when the state changes.
245    ///
246    /// # Example
247    ///
248    /// ```rust, ignore
249    /// # use freya::radio::*;
250    /// let value = radio_station.peek();
251    /// ```
252    pub fn peek(&'_ self) -> ReadRef<'_, Value> {
253        self.value.peek()
254    }
255
256    pub(crate) fn cleanup(&self) {
257        let mut listeners = self.listeners.write_unchecked();
258
259        // Clean up those channels with no reactive contexts
260        listeners.retain(|_, listeners| !listeners.borrow().is_empty());
261
262        #[cfg(feature = "tracing")]
263        {
264            use itertools::Itertools;
265            use tracing::{
266                Level,
267                info,
268                span,
269            };
270
271            let mut channels_subscribers = HashMap::<&Channel, usize>::new();
272
273            for (channel, listeners) in listeners.iter() {
274                *channels_subscribers.entry(&channel).or_default() = listeners.borrow().len();
275            }
276
277            let span = span!(Level::DEBUG, "Radio Station Metrics");
278            let _enter = span.enter();
279
280            for (channel, count) in channels_subscribers.iter().sorted() {
281                info!(" {count} subscribers for {channel:?}")
282            }
283        }
284    }
285
286    /// Modify the state using a specific channel.
287    /// This will notify all subscribers to that channel (and any derived channels).
288    ///
289    /// Returns a [`RadioGuard`] that allows direct mutation of the state.
290    /// The guard automatically notifies listeners when dropped.
291    ///
292    /// # Example
293    ///
294    /// ```rust, ignore
295    /// # use freya::radio::*;
296    /// radio_station.write_channel(MyChannel::Update).count += 1;
297    /// ```
298    pub fn write_channel(&mut self, channel: Channel) -> RadioGuard<Value, Channel> {
299        let value = self.value.write_unchecked();
300        RadioGuard {
301            channels: channel.clone().derive_channel(&*value),
302            station: *self,
303            value,
304        }
305    }
306}
307
308pub struct RadioAntenna<Value, Channel>
309where
310    Channel: RadioChannel<Value>,
311    Value: 'static,
312{
313    pub(crate) channel: Channel,
314    pub(crate) station: RadioStation<Value, Channel>,
315}
316
317impl<Value, Channel> RadioAntenna<Value, Channel>
318where
319    Channel: RadioChannel<Value>,
320{
321    pub(crate) fn new(
322        channel: Channel,
323        station: RadioStation<Value, Channel>,
324    ) -> RadioAntenna<Value, Channel> {
325        RadioAntenna { channel, station }
326    }
327}
328impl<Value, Channel> Clone for RadioAntenna<Value, Channel>
329where
330    Channel: RadioChannel<Value>,
331{
332    fn clone(&self) -> Self {
333        Self {
334            channel: self.channel.clone(),
335            station: self.station,
336        }
337    }
338}
339
340pub struct RadioGuard<Value, Channel>
341where
342    Channel: RadioChannel<Value>,
343    Value: 'static,
344{
345    pub(crate) station: RadioStation<Value, Channel>,
346    pub(crate) channels: Vec<Channel>,
347    pub(crate) value: WriteRef<'static, Value>,
348}
349
350impl<Value, Channel> Drop for RadioGuard<Value, Channel>
351where
352    Channel: RadioChannel<Value>,
353{
354    fn drop(&mut self) {
355        for channel in &mut self.channels {
356            self.station.notify_listeners(channel)
357        }
358        if !self.channels.is_empty() {
359            self.station.cleanup();
360        }
361    }
362}
363
364impl<Value, Channel> Deref for RadioGuard<Value, Channel>
365where
366    Channel: RadioChannel<Value>,
367{
368    type Target = WriteRef<'static, Value>;
369
370    fn deref(&self) -> &Self::Target {
371        &self.value
372    }
373}
374
375impl<Value, Channel> DerefMut for RadioGuard<Value, Channel>
376where
377    Channel: RadioChannel<Value>,
378{
379    fn deref_mut(&mut self) -> &mut WriteRef<'static, Value> {
380        &mut self.value
381    }
382}
383
384/// A reactive handle to the global state for a specific channel.
385/// `Radio` provides methods to read and write the global state, and automatically subscribes
386/// the current component to re-render when the associated channel is notified.
387///
388/// Each `Radio` instance is tied to a specific channel, allowing fine-grained control
389/// over which components update when the state changes.
390///
391/// # Examples
392///
393/// ## Basic usage
394///
395/// ```rust, ignore
396/// # use freya::prelude::*;
397/// # use freya::radio::*;
398///
399/// #[derive(PartialEq)]
400/// struct MyComponent {}
401///
402/// impl Component for MyComponent {
403///     fn render(&self) -> impl IntoElement {
404///         let mut radio = use_radio(MyChannel::Count);
405///
406///         rect()
407///             .child(label().text(format!("Count: {}", radio.read().count)))
408///             .child(
409///                 Button::new()
410///                     .on_press(move |_| radio.write().count += 1)
411///                     .child("Increment"),
412///             )
413///     }
414/// }
415/// ```
416///
417/// ## Using reducers
418///
419/// ```rust, ignore
420/// # use freya::prelude::*;
421/// # use freya::radio::*;
422///
423/// #[derive(Clone)]
424/// struct CounterState {
425///     count: i32,
426/// }
427///
428/// impl DataReducer for CounterState {
429///     type Channel = CounterChannel;
430///     type Action = CounterAction;
431///
432///     fn reduce(&mut self, action: CounterAction) -> ChannelSelection<CounterChannel> {
433///         match action {
434///             CounterAction::Increment => self.count += 1,
435///             CounterAction::Decrement => self.count -= 1,
436///         }
437///         ChannelSelection::Current
438///     }
439/// }
440///
441/// #[derive(PartialEq)]
442/// struct CounterComponent {}
443///
444/// impl Component for CounterComponent {
445///     fn render(&self) -> impl IntoElement {
446///         let mut radio = use_radio(CounterChannel::Count);
447///
448///         rect()
449///             .child(
450///                 Button::new()
451///                     .on_press(move |_| radio.apply(CounterAction::Increment))
452///                     .child("+"),
453///             )
454///             .child(label().text(format!("{}", radio.read().count)))
455///             .child(
456///                 Button::new()
457///                     .on_press(move |_| radio.apply(CounterAction::Decrement))
458///                     .child("-"),
459///             )
460///     }
461/// }
462/// ```
463pub struct Radio<Value, Channel>
464where
465    Channel: RadioChannel<Value>,
466    Value: 'static,
467{
468    pub(crate) antenna: State<RadioAntenna<Value, Channel>>,
469}
470
471impl<Value, Channel> Clone for Radio<Value, Channel>
472where
473    Channel: RadioChannel<Value>,
474{
475    fn clone(&self) -> Self {
476        *self
477    }
478}
479impl<Value, Channel> Copy for Radio<Value, Channel> where Channel: RadioChannel<Value> {}
480
481impl<Value, Channel> PartialEq for Radio<Value, Channel>
482where
483    Channel: RadioChannel<Value>,
484{
485    fn eq(&self, other: &Self) -> bool {
486        self.antenna == other.antenna
487    }
488}
489
490impl<Value, Channel> Radio<Value, Channel>
491where
492    Channel: RadioChannel<Value>,
493{
494    pub(crate) fn new(antenna: State<RadioAntenna<Value, Channel>>) -> Radio<Value, Channel> {
495        Radio { antenna }
496    }
497
498    pub(crate) fn subscribe_if_not(&self) {
499        if let Some(rc) = ReactiveContext::try_current() {
500            let antenna = &self.antenna.write_unchecked();
501            let channel = antenna.channel.clone();
502            let is_listening = antenna.station.is_listening(&channel, &rc);
503
504            // Subscribe the reader reactive context to the channel if it wasn't already
505            if !is_listening {
506                antenna.station.listen(channel, rc);
507            }
508        }
509    }
510
511    /// Read the current state value and subscribe the current component to changes
512    /// on this radio's channel. The component will re-render when this channel is notified.
513    ///
514    /// # Example
515    ///
516    /// ```rust, ignore
517    /// # use freya::radio::*;
518    /// let count = radio.read().count;
519    /// ```
520    pub fn read(&'_ self) -> ReadRef<'_, Value> {
521        self.subscribe_if_not();
522        self.antenna.peek().station.value.peek()
523    }
524
525    /// Read the current state value inside a callback.
526    ///
527    /// Example:
528    ///
529    /// ```rust, ignore
530    /// # use freya::radio::*;
531    /// radio.with(|value| {
532    ///     // Do something with `value`
533    /// });
534    /// ```
535    pub fn with(&self, cb: impl FnOnce(ReadRef<Value>)) {
536        self.subscribe_if_not();
537        let value = self.antenna.peek().station.value;
538        let borrow = value.read();
539        cb(borrow);
540    }
541
542    /// Get a mutable reference to the state for writing.
543    /// Changes will notify subscribers to this radio's channel.
544    ///
545    /// Returns a [`RadioGuard`] that allows direct mutation of the state.
546    ///
547    /// # Example
548    ///
549    /// ```rust, ignore
550    /// # use freya::radio::*;
551    /// radio.write().count += 1;
552    /// ```
553    pub fn write(&mut self) -> RadioGuard<Value, Channel> {
554        let value = self.antenna.peek().station.value.write_unchecked();
555        let channel = self.antenna.peek().channel.clone();
556        RadioGuard {
557            channels: channel.derive_channel(&*value),
558            station: self.antenna.read().station,
559            value,
560        }
561    }
562
563    /// Get a mutable reference to the current state value, inside a callback.
564    ///
565    /// Example:
566    ///
567    /// ```rust, ignore
568    /// # use freya::radio::*;
569    /// radio.write_with(|value| {
570    ///     // Modify `value`
571    /// });
572    /// ```
573    pub fn write_with(&mut self, cb: impl FnOnce(RadioGuard<Value, Channel>)) {
574        let guard = self.write();
575        cb(guard);
576    }
577
578    /// Modify the state using a custom Channel.
579    ///
580    /// ## Example:
581    /// ```rust, ignore
582    /// # use freya::radio::*;
583    /// radio.write(Channel::Whatever).value = 1;
584    /// ```
585    pub fn write_channel(&mut self, channel: Channel) -> RadioGuard<Value, Channel> {
586        let value = self.antenna.peek().station.value.write_unchecked();
587        RadioGuard {
588            channels: channel.derive_channel(&*value),
589            station: self.antenna.read().station,
590            value,
591        }
592    }
593
594    /// Get a mutable reference to the current state value, inside a callback.
595    ///
596    /// Example:
597    ///
598    /// ```rust, ignore
599    /// # use freya::radio::*;
600    /// radio.write_channel_with(Channel::Whatever, |value| {
601    ///     // Modify `value`
602    /// });
603    /// ```
604    pub fn write_channel_with(
605        &mut self,
606        channel: Channel,
607        cb: impl FnOnce(RadioGuard<Value, Channel>),
608    ) {
609        let guard = self.write_channel(channel);
610        cb(guard);
611    }
612
613    /// Get a mutable reference to the current state value, inside a callback that returns the channel to be used.
614    ///
615    /// Example:
616    ///
617    /// ```rust, ignore
618    /// # use freya::radio::*;
619    /// radio.write_with_channel_selection(|value| {
620    ///     // Modify `value`
621    ///     if value.cool {
622    ///         ChannelSelection::Select(Channel::Whatever)
623    ///     } else {
624    ///         ChannelSelection::Silence
625    ///     }
626    /// });
627    /// ```
628    pub fn write_with_channel_selection(
629        &mut self,
630        cb: impl FnOnce(&mut Value) -> ChannelSelection<Channel>,
631    ) -> ChannelSelection<Channel> {
632        let value = self.antenna.peek().station.value.write_unchecked();
633        let mut guard = RadioGuard {
634            channels: Vec::default(),
635            station: self.antenna.read().station,
636            value,
637        };
638        let channel_selection = cb(&mut guard.value);
639        let channel = match channel_selection.clone() {
640            ChannelSelection::Current => Some(self.antenna.peek().channel.clone()),
641            ChannelSelection::Silence => None,
642            ChannelSelection::Select(c) => Some(c),
643        };
644        if let Some(channel) = channel {
645            for channel in channel.derive_channel(&guard.value) {
646                self.antenna.peek().station.notify_listeners(&channel)
647            }
648            self.antenna.peek().station.cleanup();
649        }
650
651        channel_selection
652    }
653
654    /// Modify the state silently, no component will be notified.
655    ///
656    /// This is not recommended, the only intended usage for this is inside [RadioAsyncReducer].
657    pub fn write_silently(&mut self) -> RadioGuard<Value, Channel> {
658        let value = self.antenna.peek().station.value.write_unchecked();
659        RadioGuard {
660            channels: Vec::default(),
661            station: self.antenna.read().station,
662            value,
663        }
664    }
665}
666
667impl<Channel> Copy for ChannelSelection<Channel> where Channel: Copy {}
668
669#[derive(Clone)]
670pub enum ChannelSelection<Channel> {
671    /// Notify the channel associated with the used [Radio].
672    Current,
673    /// Notify a given `Channel`.
674    Select(Channel),
675    /// No subscriber will be notified.
676    Silence,
677}
678
679impl<Channel> ChannelSelection<Channel> {
680    /// Change to [ChannelSelection::Current]
681    pub fn current(&mut self) {
682        *self = Self::Current
683    }
684
685    /// Change to [ChannelSelection::Select]
686    pub fn select(&mut self, channel: Channel) {
687        *self = Self::Select(channel)
688    }
689
690    /// Change to [ChannelSelection::Silence]
691    pub fn silence(&mut self) {
692        *self = Self::Silence
693    }
694
695    /// Check if it is of type [ChannelSelection::Current]
696    pub fn is_current(&self) -> bool {
697        matches!(self, Self::Current)
698    }
699
700    /// Check if it is of type [ChannelSelection::Select] and return the channel.
701    pub fn is_select(&self) -> Option<&Channel> {
702        match self {
703            Self::Select(channel) => Some(channel),
704            _ => None,
705        }
706    }
707
708    /// Check if it is of type [ChannelSelection::Silence]
709    pub fn is_silence(&self) -> bool {
710        matches!(self, Self::Silence)
711    }
712}
713
714/// Provide an existing [`RadioStation`] to descendant components.
715/// This is useful for sharing the same global state across different parts of the component tree
716/// or across multiple windows.
717pub fn use_share_radio<Value, Channel>(radio: impl FnOnce() -> RadioStation<Value, Channel>)
718where
719    Channel: RadioChannel<Value>,
720    Value: 'static,
721{
722    use_provide_context(radio);
723}
724
725/// Subscribe to the global state for a specific channel.
726/// Returns a [`Radio`] handle that allows reading and writing the state.
727/// The current component will re-render whenever the specified channel is notified.
728///
729/// This hook must be called within a component that has access to a [`RadioStation`]
730/// (either through [`use_init_radio_station`] or [`use_share_radio`]).
731///
732/// # Example
733///
734/// ```rust, ignore
735/// # use freya::prelude::*;
736/// # use freya::radio::*;
737///
738/// fn app() -> impl IntoElement {
739///     use_init_radio_station::<AppState, AppChannel>(AppState::default);
740///
741///     rect().child(Counter {})
742/// }
743///
744/// #[derive(PartialEq)]
745/// struct Counter {}
746///
747/// impl Component for Counter {
748///     fn render(&self) -> impl IntoElement {
749///         let mut radio = use_radio(AppChannel::Count);
750///
751///         rect()
752///             .child(label().text(format!("Count: {}", radio.read().count)))
753///             .child(
754///                 Button::new()
755///                     .on_press(move |_| radio.write().count += 1)
756///                     .child("+"),
757///             )
758///     }
759/// }
760/// ```
761pub fn use_radio<Value, Channel>(channel: Channel) -> Radio<Value, Channel>
762where
763    Channel: RadioChannel<Value>,
764    Value: 'static,
765{
766    let station = use_consume::<RadioStation<Value, Channel>>();
767
768    let mut radio = use_hook(|| {
769        let antenna = RadioAntenna::new(channel.clone(), station);
770        Radio::new(State::create(antenna))
771    });
772
773    if radio.antenna.peek().channel != channel {
774        radio.antenna.write().channel = channel;
775    }
776
777    radio
778}
779
780/// Initialize a new radio station in the current component tree.
781/// This provides the global state to all descendant components.
782///
783/// Returns the [`RadioStation`] instance for direct access if needed.
784///
785/// # Example
786///
787/// ```rust, ignore
788/// # use freya::prelude::*;
789/// # use freya::radio::*;
790///
791/// fn app() -> impl IntoElement {
792///     use_init_radio_station::<AppState, AppChannel>(AppState::default);
793///
794///     rect().child(MyComponent {})
795/// }
796/// ```
797pub fn use_init_radio_station<Value, Channel>(
798    init_value: impl FnOnce() -> Value,
799) -> RadioStation<Value, Channel>
800where
801    Channel: RadioChannel<Value>,
802    Value: 'static,
803{
804    use_provide_context(|| RadioStation::create(init_value()))
805}
806
807pub fn use_radio_station<Value, Channel>() -> RadioStation<Value, Channel>
808where
809    Channel: RadioChannel<Value>,
810    Value: 'static,
811{
812    use_consume::<RadioStation<Value, Channel>>()
813}
814
815/// Trait for implementing a reducer pattern on your state.
816/// Reducers allow you to define actions that modify the state in a controlled way.
817///
818/// Implement this trait on your state type to enable the [`RadioReducer`] functionality.
819///
820/// # Example
821///
822/// ```rust, ignore
823/// # use freya::radio::*;
824///
825/// #[derive(Clone)]
826/// struct Counter {
827///     count: i32,
828/// }
829///
830/// #[derive(Clone)]
831/// enum CounterAction {
832///     Increment,
833///     Decrement,
834///     Set(i32),
835/// }
836///
837/// impl DataReducer for Counter {
838///     type Channel = CounterChannel;
839///     type Action = CounterAction;
840///
841///     fn reduce(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel> {
842///         match action {
843///             CounterAction::Increment => self.count += 1,
844///             CounterAction::Decrement => self.count -= 1,
845///             CounterAction::Set(value) => self.count = value,
846///         }
847///         ChannelSelection::Current
848///     }
849/// }
850/// ```
851pub trait DataReducer {
852    type Channel;
853    type Action;
854
855    fn reduce(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel>;
856}
857
858pub trait RadioReducer {
859    type Action;
860    type Channel;
861
862    fn apply(&mut self, action: Self::Action) -> ChannelSelection<Self::Channel>;
863}
864
865impl<Data: DataReducer<Channel = Channel, Action = Action>, Channel: RadioChannel<Data>, Action>
866    RadioReducer for Radio<Data, Channel>
867{
868    type Action = Action;
869    type Channel = Channel;
870
871    fn apply(&mut self, action: Action) -> ChannelSelection<Channel> {
872        self.write_with_channel_selection(|data| data.reduce(action))
873    }
874}
875
876pub trait DataAsyncReducer {
877    type Channel;
878    type Action;
879
880    #[allow(async_fn_in_trait)]
881    async fn async_reduce(
882        _radio: &mut Radio<Self, Self::Channel>,
883        _action: Self::Action,
884    ) -> ChannelSelection<Self::Channel>
885    where
886        Self::Channel: RadioChannel<Self>,
887        Self: Sized;
888}
889
890pub trait RadioAsyncReducer {
891    type Action;
892
893    fn async_apply(&mut self, _action: Self::Action)
894    where
895        Self::Action: 'static;
896}
897
898impl<
899    Data: DataAsyncReducer<Channel = Channel, Action = Action>,
900    Channel: RadioChannel<Data>,
901    Action,
902> RadioAsyncReducer for Radio<Data, Channel>
903{
904    type Action = Action;
905
906    fn async_apply(&mut self, action: Self::Action)
907    where
908        Self::Action: 'static,
909    {
910        let mut radio = *self;
911        spawn(async move {
912            let channel = Data::async_reduce(&mut radio, action).await;
913            radio.write_with_channel_selection(|_| channel);
914        });
915    }
916}