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}