waterui_core/components/anyview.rs
1//! This module provides type-erased view implementations to enable
2//! heterogeneous collections of views and dynamic dispatch.
3//!
4//! The main type provided by this module is [`AnyView`], which wraps
5//! any type implementing the [`View`] trait and erases its concrete type
6//! while preserving its behavior.
7
8use core::fmt;
9use core::{
10 any::{Any, TypeId, type_name},
11 fmt::Debug,
12 ptr,
13};
14
15use alloc::boxed::Box;
16
17use crate::{Environment, View, layout::StretchAxis};
18
19trait AnyViewImpl: 'static {
20 fn body(self: Box<Self>, env: Environment) -> AnyView;
21 fn type_id(&self) -> TypeId {
22 TypeId::of::<Self>()
23 }
24 fn name(&self) -> &'static str {
25 type_name::<Self>()
26 }
27 fn stretch_axis(&self) -> StretchAxis;
28}
29
30impl<T: View> AnyViewImpl for T {
31 fn body(self: Box<Self>, env: Environment) -> AnyView {
32 AnyView::new(View::body(*self, &env))
33 }
34 fn stretch_axis(&self) -> StretchAxis {
35 View::stretch_axis(self)
36 }
37}
38
39/// A type-erased wrapper for a `View`.
40///
41/// This allows storing and passing around different view types uniformly.
42#[must_use]
43pub struct AnyView(Box<dyn AnyViewImpl>);
44
45impl Debug for AnyView {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 f.write_fmt(format_args!("AnyView({})", self.name()))
48 }
49}
50
51impl Default for AnyView {
52 fn default() -> Self {
53 Self::new(())
54 }
55}
56
57impl AnyView {
58 /// Creates a new `AnyView` from any type that implements `View`.
59 ///
60 /// If the provided view is already an `AnyView`, it will be unwrapped
61 /// to avoid unnecessary nesting.
62 pub fn new<V: View>(view: V) -> Self {
63 #[allow(clippy::missing_panics_doc)]
64 if TypeId::of::<V>() == TypeId::of::<Self>() {
65 let any = &mut Some(view) as &mut dyn Any;
66 return any
67 .downcast_mut::<Option<Self>>()
68 .expect("downcast to option should succeed")
69 .take()
70 .expect("option should contain a value"); // TODO: use downcast_mut_unchecked when it's stable
71 }
72
73 Self(Box::new(view))
74 }
75
76 /// Checks if the contained view is of type `T`.
77 #[must_use]
78 pub fn is<T: 'static>(&self) -> bool {
79 self.type_id() == TypeId::of::<T>()
80 }
81
82 /// Returns the `TypeId` of the contained view.
83 #[must_use]
84 pub fn type_id(&self) -> TypeId {
85 AnyViewImpl::type_id(&*self.0)
86 }
87
88 /// Returns the type name of the contained view.
89 #[must_use]
90 pub fn name(&self) -> &'static str {
91 AnyViewImpl::name(&*self.0)
92 }
93
94 /// Returns the stretch axis of the contained view.
95 ///
96 /// This delegates to the `View::stretch_axis()` method of the wrapped view,
97 /// which for native views returns their layout stretch behavior.
98 #[must_use]
99 pub fn stretch_axis(&self) -> StretchAxis {
100 AnyViewImpl::stretch_axis(&*self.0)
101 }
102
103 #[doc(hidden)]
104 #[must_use]
105 pub fn stable_ptr(&self) -> *const () {
106 ptr::from_ref::<dyn AnyViewImpl>(&*self.0).cast::<()>()
107 }
108
109 /// Downcasts `AnyView` to a concrete view type without any runtime checks.
110 ///
111 /// # Safety
112 /// Calling this method with the incorrect type is undefined behavior.
113 #[must_use]
114 pub unsafe fn downcast_unchecked<T: 'static>(self) -> Box<T> {
115 // SAFETY: the caller contract requires the erased value to be a `T`, so the
116 // box being re-typed has the layout it is cast to; ownership moves across
117 // unchanged.
118 unsafe { Box::from_raw(Box::into_raw(self.0).cast::<T>()) }
119 }
120
121 /// Returns a reference to the contained view without any runtime checks.
122 ///
123 /// # Safety
124 /// Calling this method with the incorrect type is undefined behavior.
125 #[must_use]
126 pub const unsafe fn downcast_ref_unchecked<T: 'static>(&self) -> &T {
127 // SAFETY: the caller contract requires the erased value to be a `T`; the
128 // borrow is tied to `&self`.
129 unsafe { &*(&raw const *self.0).cast::<T>() }
130 }
131
132 /// Returns a mutable reference to the contained view without any runtime checks.
133 ///
134 /// # Safety
135 /// Calling this method with the incorrect type is undefined behavior.
136 pub const unsafe fn downcast_mut_unchecked<T: 'static>(&mut self) -> &mut T {
137 // SAFETY: the caller contract requires the erased value to be a `T`; the
138 // borrow is tied to `&mut self`, so it is exclusive.
139 unsafe { &mut *(&raw mut *self.0).cast::<T>() }
140 }
141
142 /// Attempts to downcast `AnyView` to a concrete view type.
143 ///
144 /// Returns `Ok` with the boxed value if the types match, or
145 /// `Err` with the original `AnyView` if the types don't match.
146 ///
147 /// # Errors
148 ///
149 /// Returns `Err(Self)` if the contained type does not match `T`.
150 pub fn downcast<T: 'static>(self) -> Result<Box<T>, Self> {
151 if self.is::<T>() {
152 // SAFETY: the `is::<T>()` check above proves the erased value is a `T`.
153 unsafe { Ok(self.downcast_unchecked()) }
154 } else {
155 Err(self)
156 }
157 }
158
159 /// Attempts to get a reference to the contained view of a specific type.
160 ///
161 /// Returns `Some` if the types match, or `None` if they don't.
162 #[must_use]
163 pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
164 // SAFETY: the closure only runs when `is::<T>()` holds, which is exactly the
165 // unchecked accessor's requirement.
166 unsafe { self.is::<T>().then(|| self.downcast_ref_unchecked()) }
167 }
168
169 /// Attempts to get a mutable reference to the contained view of a specific type.
170 ///
171 /// Returns `Some` if the types match, or `None` if they don't.
172 pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
173 // SAFETY: as above — the closure only runs when the type matches.
174 unsafe { self.is::<T>().then(move || self.downcast_mut_unchecked()) }
175 }
176}
177
178impl View for AnyView {
179 fn body(self, env: &Environment) -> impl View {
180 self.0.body(env.clone())
181 }
182
183 /// Forwards the erased view's own answer.
184 ///
185 /// Without this, erasing a view silently reset its stretch axis to the
186 /// default: the inherent [`AnyView::stretch_axis`] reported the real value
187 /// while the trait method — the one generic code calls — reported `None`.
188 /// Every container that wanted its content's axis therefore had to copy it
189 /// before erasing, and that copy is what went stale.
190 fn stretch_axis(&self) -> StretchAxis {
191 AnyViewImpl::stretch_axis(&*self.0)
192 }
193}
194
195#[cfg(test)]
196mod test {
197 use core::any::TypeId;
198
199 use super::AnyView;
200
201 #[test]
202 pub fn get_type_id() {
203 assert_eq!(AnyView::new(()).type_id(), TypeId::of::<()>());
204 }
205}