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