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}