Skip to main content

waterui_core/components/
dynamic.rs

1//! Dynamic views that can be updated at runtime.
2//!
3//! This module provides components for creating views that can change their content
4//! based on reactive state or explicit updates.
5//!
6//! - `Dynamic` - A view that can be updated through a `DynamicHandler`
7//! - `watch` - Helper for the exceptional case where reactive state changes view structure
8//!
9//! # Examples
10//!
11//! ```rust
12//! use waterui_core::{dynamic::{Dynamic, watch}, Binding};
13//!
14//! // Create a dynamic view with a handler
15//! let (handler, view) = Dynamic::new();
16//! handler.set("Initial content");
17//!
18//! // Replace a subtree only when the semantic view type genuinely changes.
19//! let show_details = Binding::container(false);
20//! let content = watch(show_details, |show| {
21//!     if show { "Details" } else { "Summary" }
22//! });
23use crate::components::metadata::Retain;
24use crate::{AnyView, Environment, Metadata, View};
25use alloc::boxed::Box;
26use alloc::rc::Rc;
27use core::cell::RefCell;
28use core::marker::PhantomData;
29use nami::watcher::Context;
30use nami::{Signal, watcher::Metadata as WatcherMetadata};
31
32/// A dynamic view that can be updated.
33///
34/// Represents a view whose content can be changed dynamically at runtime.
35///
36/// You should avoid using this component if possible,
37/// most of components in `WaterUI` already provide a way to update their content reactively.
38#[derive(Clone)]
39pub struct Dynamic(DynamicHandler);
40
41raw_view!(Dynamic);
42
43/// A handler for updating a Dynamic view.
44///
45/// Provides methods to set new content for the associated Dynamic view.
46#[derive(Clone)]
47pub struct DynamicHandler(Rc<RefCell<DynamicHandlerState>>);
48
49enum DynamicHandlerState {
50    /// Connected to a receiver (Swift/native side).
51    Connected {
52        receiver: Receiver,
53        pending_view_slot: Option<Rc<RefCell<Option<AnyView>>>>,
54    },
55    /// Not yet connected, stores the initial view if set before connection.
56    Unconnected(Option<AnyView>),
57}
58
59type Receiver = Box<dyn Fn(Context<AnyView>)>;
60
61/// Metadata marker for the body-time snapshot installed by [`Dynamic::watch`].
62#[derive(Clone, Copy, Debug)]
63pub struct DynamicInitialContent;
64
65impl_debug!(Dynamic);
66impl_debug!(DynamicHandler);
67
68impl DynamicHandler {
69    /// Sets the content of the Dynamic view with the provided view and metadata.
70    ///
71    /// # Arguments
72    ///
73    /// * `view` - The new view to display
74    /// * `metadata` - Additional metadata associated with the update
75    pub fn set_with_metadata(&self, view: impl View, metadata: WatcherMetadata) {
76        let mut state = self.0.borrow_mut();
77        let view = AnyView::new(view);
78        match &mut *state {
79            DynamicHandlerState::Connected { receiver, .. } => {
80                receiver(Context::new(view, metadata));
81            }
82            DynamicHandlerState::Unconnected(temp_view) => {
83                *temp_view = Some(view);
84            }
85        }
86    }
87
88    /// Sets the content of the Dynamic view with the provided view.
89    ///
90    /// # Arguments
91    ///
92    /// * `view` - The new view to display
93    pub fn set(&self, view: impl View) {
94        self.set_with_metadata(view, WatcherMetadata::new());
95    }
96}
97
98impl Dynamic {
99    /// Creates a new Dynamic view along with its handler.
100    ///
101    /// Returns a tuple of (handler, view) where the handler can be used to update
102    /// the view's content.
103    ///
104    /// # Returns
105    ///
106    /// A tuple containing the [`DynamicHandler`] and Dynamic view
107    #[must_use]
108    pub fn new() -> (DynamicHandler, Self) {
109        let handler = DynamicHandler(Rc::new(RefCell::new(DynamicHandlerState::Unconnected(
110            None,
111        ))));
112        (handler.clone(), Self(handler))
113    }
114
115    /// Creates a Dynamic view that watches structural reactive state.
116    ///
117    /// The provided function is used to convert the value to a view.
118    /// Whenever the watched value changes, the entire child subtree is replaced.
119    /// State owned by the replaced subtree is discarded. Prefer signal-aware
120    /// component inputs, modifiers, metadata, and reactive collections for scalar
121    /// values or collection membership. Use this only when the semantic view
122    /// structure itself must change.
123    ///
124    /// # Arguments
125    ///
126    /// * `value` - The reactive value to watch
127    /// * `f` - A function that converts the value to a view
128    ///
129    /// # Returns
130    ///
131    /// A Dynamic view that updates when the value changes
132    pub fn watch<T, S, V: View>(value: S, f: impl 'static + Fn(T) -> V) -> impl View
133    where
134        S: Signal<Output = T> + 'static,
135        T: 'static,
136    {
137        WatchedDynamic {
138            value,
139            f,
140            marker: PhantomData,
141        }
142    }
143
144    /// Connects the Dynamic view to a receiver function.
145    ///
146    /// For internal use only.
147    ///
148    /// The receiver function is called whenever the view content is updated.
149    /// If there's a temporary view stored (set before connecting), it will
150    /// be immediately passed to the receiver.
151    ///
152    /// # Arguments
153    ///
154    /// * `receiver` - A function that receives view updates
155    pub fn connect(self, receiver: impl Fn(Context<AnyView>) + 'static) {
156        self.connect_internal(None, receiver);
157    }
158
159    /// Connects the dynamic node while preserving a pending measurement view.
160    ///
161    /// This is used by renderers that need to stage a temporary child view
162    /// before the final backend receiver is attached.
163    pub fn connect_with_pending_view(
164        self,
165        pending_view_slot: Rc<RefCell<Option<AnyView>>>,
166        receiver: impl Fn(Context<AnyView>) + 'static,
167    ) {
168        self.connect_internal(Some(pending_view_slot), receiver);
169    }
170
171    fn connect_internal(
172        self,
173        pending_view_slot: Option<Rc<RefCell<Option<AnyView>>>>,
174        receiver: impl Fn(Context<AnyView>) + 'static,
175    ) {
176        let mut state = self.0.0.borrow_mut();
177
178        match &mut *state {
179            DynamicHandlerState::Unconnected(temp_view) => {
180                if let Some(view) = temp_view.take() {
181                    receiver(Context::new(view, WatcherMetadata::new()));
182                }
183                *state = DynamicHandlerState::Connected {
184                    receiver: Box::new(receiver),
185                    pending_view_slot,
186                };
187            }
188            DynamicHandlerState::Connected { .. } => unreachable!("Dynamic already connected"),
189        }
190    }
191
192    /// Returns a stable identity for this dynamic node.
193    #[must_use]
194    pub fn identity(&self) -> usize {
195        Rc::as_ptr(&self.0.0) as usize
196    }
197
198    /// Reads the current pre-connection view snapshot, if this dynamic node
199    /// has not been connected yet.
200    ///
201    /// Returns `None` when the node is already connected to a backend receiver.
202    pub fn with_unconnected_view<R>(&self, f: impl FnOnce(Option<&AnyView>) -> R) -> Option<R> {
203        let state = self.0.0.borrow();
204        match &*state {
205            DynamicHandlerState::Unconnected(view) => Some(f(view.as_ref())),
206            DynamicHandlerState::Connected { .. } => None,
207        }
208    }
209
210    /// Mutates the current pre-connection view snapshot before the dynamic node connects.
211    ///
212    /// Returns `None` when the node is already connected to a backend receiver.
213    pub fn with_unconnected_view_mut<R>(
214        &self,
215        f: impl FnOnce(&mut Option<AnyView>) -> R,
216    ) -> Option<R> {
217        let mut state = self.0.0.borrow_mut();
218        match &mut *state {
219            DynamicHandlerState::Unconnected(view) => Some(f(view)),
220            DynamicHandlerState::Connected { .. } => None,
221        }
222    }
223
224    /// Mutates whichever view snapshot should be used for layout measurement.
225    ///
226    /// Before connection this is the unconnected snapshot; after connection it
227    /// targets the pending connected snapshot when one exists.
228    pub fn with_measurement_view_mut<R>(
229        &self,
230        f: impl FnOnce(&mut Option<AnyView>) -> R,
231    ) -> Option<R> {
232        let mut state = self.0.0.borrow_mut();
233        match &mut *state {
234            DynamicHandlerState::Unconnected(view) => Some(f(view)),
235            DynamicHandlerState::Connected {
236                pending_view_slot: Some(slot),
237                ..
238            } => Some(f(&mut slot.borrow_mut())),
239            DynamicHandlerState::Connected {
240                pending_view_slot: None,
241                ..
242            } => None,
243        }
244    }
245
246    /// Mutates the pending connected snapshot without affecting the
247    /// pre-connection snapshot.
248    pub fn with_connected_pending_view_mut<R>(
249        &self,
250        f: impl FnOnce(&mut Option<AnyView>) -> R,
251    ) -> Option<R> {
252        let mut state = self.0.0.borrow_mut();
253        match &mut *state {
254            DynamicHandlerState::Connected {
255                pending_view_slot: Some(slot),
256                ..
257            } => Some(f(&mut slot.borrow_mut())),
258            DynamicHandlerState::Connected {
259                pending_view_slot: None,
260                ..
261            }
262            | DynamicHandlerState::Unconnected(_) => None,
263        }
264    }
265}
266
267struct WatchedDynamic<T, S, F> {
268    value: S,
269    f: F,
270    marker: PhantomData<fn(T)>,
271}
272
273impl<T, S, F, V> View for WatchedDynamic<T, S, F>
274where
275    T: 'static,
276    S: Signal<Output = T> + 'static,
277    F: Fn(T) -> V + 'static,
278    V: View + 'static,
279{
280    fn body(self, _env: &Environment) -> impl View {
281        let (handle, dynamic) = Dynamic::new();
282        let f = Rc::new(self.f);
283
284        handle.set_with_metadata(
285            f(self.value.get()),
286            WatcherMetadata::new().with(DynamicInitialContent),
287        );
288
289        let guard = self.value.watch({
290            let f = Rc::clone(&f);
291            move |value| handle.set(f(value.into_value()))
292        });
293
294        Metadata::new(dynamic, Retain::new((guard, self.value)))
295    }
296}
297
298/// Creates a view that watches structural reactive state.
299///
300/// A convenience function that calls [`Dynamic::watch`].
301///
302/// # Arguments
303///
304/// * `value` - The reactive value to watch
305/// * `f` - A function that converts the value to a view
306///
307/// # Returns
308///
309/// A view whose entire child subtree is replaced when the value changes
310pub fn watch<T: 'static, S, V: View>(value: S, f: impl Fn(T) -> V + 'static) -> impl View
311where
312    S: Signal<Output = T> + 'static,
313{
314    Dynamic::watch(value, f)
315}