Skip to main content

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}