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