waterui-core 0.5.1

Core functionality for the WaterUI framework
Documentation
//! This module provides type-erased view implementations to enable
//! heterogeneous collections of views and dynamic dispatch.
//!
//! The main type provided by this module is [`AnyView`], which wraps
//! any type implementing the [`View`] trait and erases its concrete type
//! while preserving its behavior.

use core::fmt;
use core::{
    any::{Any, TypeId, type_name},
    fmt::Debug,
    ptr,
};

use alloc::boxed::Box;

use crate::{Environment, View, layout::StretchAxis};

trait AnyViewImpl: 'static {
    fn body(self: Box<Self>, env: Environment) -> AnyView;
    fn type_id(&self) -> TypeId {
        TypeId::of::<Self>()
    }
    fn name(&self) -> &'static str {
        type_name::<Self>()
    }
    fn stretch_axis(&self) -> StretchAxis;
}

impl<T: View> AnyViewImpl for T {
    fn body(self: Box<Self>, env: Environment) -> AnyView {
        AnyView::new(View::body(*self, &env))
    }
    fn stretch_axis(&self) -> StretchAxis {
        View::stretch_axis(self)
    }
}

/// A type-erased wrapper for a `View`.
///
/// This allows storing and passing around different view types uniformly.
#[must_use]
pub struct AnyView(Box<dyn AnyViewImpl>);

impl Debug for AnyView {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("AnyView({})", self.name()))
    }
}

impl Default for AnyView {
    fn default() -> Self {
        Self::new(())
    }
}

impl AnyView {
    /// Creates a new `AnyView` from any type that implements `View`.
    ///
    /// If the provided view is already an `AnyView`, it will be unwrapped
    /// to avoid unnecessary nesting.
    pub fn new<V: View>(view: V) -> Self {
        #[allow(clippy::missing_panics_doc)]
        if TypeId::of::<V>() == TypeId::of::<Self>() {
            let any = &mut Some(view) as &mut dyn Any;
            return any
                .downcast_mut::<Option<Self>>()
                .expect("downcast to option should succeed")
                .take()
                .expect("option should contain a value"); // TODO: use downcast_mut_unchecked when it's stable
        }

        Self(Box::new(view))
    }

    /// Checks if the contained view is of type `T`.
    #[must_use]
    pub fn is<T: 'static>(&self) -> bool {
        self.type_id() == TypeId::of::<T>()
    }

    /// Returns the `TypeId` of the contained view.
    #[must_use]
    pub fn type_id(&self) -> TypeId {
        AnyViewImpl::type_id(&*self.0)
    }

    /// Returns the type name of the contained view.
    #[must_use]
    pub fn name(&self) -> &'static str {
        AnyViewImpl::name(&*self.0)
    }

    /// Returns the stretch axis of the contained view.
    ///
    /// This delegates to the `View::stretch_axis()` method of the wrapped view,
    /// which for native views returns their layout stretch behavior.
    #[must_use]
    pub fn stretch_axis(&self) -> StretchAxis {
        AnyViewImpl::stretch_axis(&*self.0)
    }

    #[doc(hidden)]
    #[must_use]
    pub fn stable_ptr(&self) -> *const () {
        ptr::from_ref::<dyn AnyViewImpl>(&*self.0).cast::<()>()
    }

    /// Downcasts `AnyView` to a concrete view type without any runtime checks.
    ///
    /// # Safety
    /// Calling this method with the incorrect type is undefined behavior.
    #[must_use]
    pub unsafe fn downcast_unchecked<T: 'static>(self) -> Box<T> {
        // SAFETY: the caller contract requires the erased value to be a `T`, so the
        // box being re-typed has the layout it is cast to; ownership moves across
        // unchanged.
        unsafe { Box::from_raw(Box::into_raw(self.0).cast::<T>()) }
    }

    /// Returns a reference to the contained view without any runtime checks.
    ///
    /// # Safety
    /// Calling this method with the incorrect type is undefined behavior.
    #[must_use]
    pub const unsafe fn downcast_ref_unchecked<T: 'static>(&self) -> &T {
        // SAFETY: the caller contract requires the erased value to be a `T`; the
        // borrow is tied to `&self`.
        unsafe { &*(&raw const *self.0).cast::<T>() }
    }

    /// Returns a mutable reference to the contained view without any runtime checks.
    ///
    /// # Safety
    /// Calling this method with the incorrect type is undefined behavior.
    pub const unsafe fn downcast_mut_unchecked<T: 'static>(&mut self) -> &mut T {
        // SAFETY: the caller contract requires the erased value to be a `T`; the
        // borrow is tied to `&mut self`, so it is exclusive.
        unsafe { &mut *(&raw mut *self.0).cast::<T>() }
    }

    /// Attempts to downcast `AnyView` to a concrete view type.
    ///
    /// Returns `Ok` with the boxed value if the types match, or
    /// `Err` with the original `AnyView` if the types don't match.
    ///
    /// # Errors
    ///
    /// Returns `Err(Self)` if the contained type does not match `T`.
    pub fn downcast<T: 'static>(self) -> Result<Box<T>, Self> {
        if self.is::<T>() {
            // SAFETY: the `is::<T>()` check above proves the erased value is a `T`.
            unsafe { Ok(self.downcast_unchecked()) }
        } else {
            Err(self)
        }
    }

    /// Attempts to get a reference to the contained view of a specific type.
    ///
    /// Returns `Some` if the types match, or `None` if they don't.
    #[must_use]
    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
        // SAFETY: the closure only runs when `is::<T>()` holds, which is exactly the
        // unchecked accessor's requirement.
        unsafe { self.is::<T>().then(|| self.downcast_ref_unchecked()) }
    }

    /// Attempts to get a mutable reference to the contained view of a specific type.
    ///
    /// Returns `Some` if the types match, or `None` if they don't.
    pub fn downcast_mut<T: 'static>(&mut self) -> Option<&mut T> {
        // SAFETY: as above — the closure only runs when the type matches.
        unsafe { self.is::<T>().then(move || self.downcast_mut_unchecked()) }
    }
}

impl View for AnyView {
    fn body(self, env: &Environment) -> impl View {
        self.0.body(env.clone())
    }

    /// Forwards the erased view's own answer.
    ///
    /// Without this, erasing a view silently reset its stretch axis to the
    /// default: the inherent [`AnyView::stretch_axis`] reported the real value
    /// while the trait method — the one generic code calls — reported `None`.
    /// Every container that wanted its content's axis therefore had to copy it
    /// before erasing, and that copy is what went stale.
    fn stretch_axis(&self) -> StretchAxis {
        AnyViewImpl::stretch_axis(&*self.0)
    }
}

#[cfg(test)]
mod test {
    use core::any::TypeId;

    use super::AnyView;

    #[test]
    pub fn get_type_id() {
        assert_eq!(AnyView::new(()).type_id(), TypeId::of::<()>());
    }
}