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