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