waterui-layout 0.5.1

Layout components for WaterUI
Documentation
//! Background layout for rendering content behind a view.
//!
//! `background` is the inverse of `overlay` - the background view renders
//! behind the content, and the content determines the layout size.
//!
//! # Example
//!
//! ```rust
//! use waterui::prelude::*;
//!
//! let gradient_view = hstack((Color::blue(), Color::teal()));
//!
//! // Gradient behind text
//! text!("Hello").background(gradient_view);
//!
//! // Using the layout directly
//! background(text!("Hello"), Color::blue());
//! ```

use core::fmt;

use alloc::vec::Vec;
use waterui_core::View;

use crate::{
    HorizontalAlignment, Layout, PlacedSubview, ProposalSize, Rect, Size, StretchAxis, SubView,
    SubviewPlacement, VerticalAlignment, container::FixedContainer,
};

/// Layout used by [`BackgroundView`] to render a background behind content.
///
/// The content child determines the size, the background fills the bounds.
/// Children order: `[background, content]`
#[derive(Debug, Clone, Copy, Default)]
pub struct BackgroundLayout;

impl Layout for BackgroundLayout {
    /// A background is transparent to its content: `children[1]`. Answering from
    /// the child rather than from a copy taken at build time keeps the two from
    /// disagreeing.
    fn stretch_axis(&self, children: &[StretchAxis]) -> StretchAxis {
        children.get(1).copied().unwrap_or_default()
    }

    fn size_that_fits(&self, proposal: ProposalSize, children: &[&dyn SubView]) -> Size {
        // Size is driven entirely by the content child (index 1).
        // Background just fills whatever size content needs.
        // children[0] = background, children[1] = content
        children
            .get(1)
            .expect("BackgroundLayout requires a content child at index 1")
            .measure(proposal)
            .size
    }

    fn place(
        &self,
        bounds: Rect,
        _proposal: ProposalSize,
        children: &[&dyn SubView],
    ) -> Vec<SubviewPlacement> {
        assert!(
            children.len() >= 2,
            "BackgroundLayout requires at least 2 children: background and content"
        );

        let mut placements = Vec::with_capacity(children.len());

        // Background (index 0) fills the entire bounds; it is offered the
        // resolved finite bounds, not the incoming proposal.
        placements.push(SubviewPlacement::new(
            Rect::new(bounds.origin(), Size::new(bounds.width(), bounds.height())),
            ProposalSize::new(bounds.width(), bounds.height()),
        ));

        // Content (index 1) also fills the bounds and, like every child at
        // placement, is proposed the resolved bounds it is placed in.
        placements.push(SubviewPlacement::new(
            Rect::new(bounds.origin(), Size::new(bounds.width(), bounds.height())),
            ProposalSize::new(bounds.width(), bounds.height()),
        ));

        placements
    }

    fn explicit_horizontal(
        &self,
        alignment: HorizontalAlignment,
        _bounds: Rect,
        children: &[PlacedSubview<'_>],
    ) -> Option<f32> {
        children
            .get(1)
            .and_then(|child| child.explicit_horizontal(alignment))
    }

    fn explicit_vertical(
        &self,
        alignment: VerticalAlignment,
        _bounds: Rect,
        children: &[PlacedSubview<'_>],
    ) -> Option<f32> {
        children
            .get(1)
            .and_then(|child| child.explicit_vertical(alignment))
    }
}

/// A view that renders a `background` behind the `content`.
///
/// The content determines the layout size, and the background fills the bounds.
/// The background renders first (behind), then content renders on top.
pub struct BackgroundView<Content, Bg> {
    content: Content,
    background: Bg,
}

impl<Content: View, Bg: View> BackgroundView<Content, Bg> {
    /// Creates a new background view with the provided content and background.
    #[must_use]
    pub const fn new(content: Content, background: Bg) -> Self {
        Self {
            content,
            background,
        }
    }
}

impl<Content, Bg> fmt::Debug for BackgroundView<Content, Bg> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BackgroundView").finish_non_exhaustive()
    }
}

impl<Content, Bg> View for BackgroundView<Content, Bg>
where
    Content: View + 'static,
    Bg: View + 'static,
{
    fn body(self, _env: &waterui_core::Environment) -> impl View {
        let Self {
            content,
            background,
        } = self;

        // Background is first (renders behind), content is second (renders on top)
        FixedContainer::new(BackgroundLayout, (background, content))
    }

    /// Resolves to `FixedContainer` over `BackgroundLayout` and the two
    /// children in `[background, content]` order; reports what that container
    /// would — `BackgroundLayout` is transparent to the content child's axis.
    fn stretch_axis(&self) -> StretchAxis {
        BackgroundLayout
            .stretch_axis(&[self.background.stretch_axis(), self.content.stretch_axis()])
    }
}

/// Convenience constructor for creating a [`BackgroundView`].
#[must_use]
pub const fn background<Content: View, Bg: View>(
    content: Content,
    background: Bg,
) -> BackgroundView<Content, Bg> {
    BackgroundView::new(content, background)
}

#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
    use waterui_core::layout::Point;
    use waterui_core::layout::ViewDimensions;

    use super::*;
    use alloc::vec;

    struct MockSubView {
        size: Size,
    }

    impl SubView for MockSubView {
        fn measure(&self, _proposal: ProposalSize) -> ViewDimensions {
            ViewDimensions::new(self.size)
        }
        fn stretch_axis(&self) -> StretchAxis {
            StretchAxis::None
        }
        fn priority(&self) -> i32 {
            0
        }
    }

    #[test]
    fn test_background_size_from_content() {
        let layout = BackgroundLayout;

        let mut bg = MockSubView {
            size: Size::new(200.0, 200.0),
        };
        let mut content = MockSubView {
            size: Size::new(100.0, 50.0),
        };

        let children: Vec<&dyn SubView> = vec![&mut bg, &mut content];
        let size = layout.size_that_fits(ProposalSize::UNSPECIFIED, &children);

        // Size comes from content child (index 1)
        assert_eq!(size.width, 100.0);
        assert_eq!(size.height, 50.0);
    }

    #[test]
    fn test_background_fills_bounds_bounded_proposal() {
        let layout = BackgroundLayout;

        let mut bg = MockSubView {
            size: Size::new(50.0, 50.0),
        };
        let mut content = MockSubView {
            size: Size::new(100.0, 100.0),
        };

        let children: Vec<&dyn SubView> = vec![&mut bg, &mut content];
        let bounds = Rect::new(Point::new(0.0, 0.0), Size::new(100.0, 100.0));
        let proposal = ProposalSize::new(Some(bounds.width()), Some(bounds.height()));
        let placements = layout.place(bounds, proposal, &children);

        // Both fill the bounds
        assert_eq!(placements[0].frame.width(), 100.0);
        assert_eq!(placements[0].frame.height(), 100.0);
        assert_eq!(placements[1].frame.width(), 100.0);
        assert_eq!(placements[1].frame.height(), 100.0);
    }

    #[test]
    fn test_background_layout_reports_its_content_stretch_axis() {
        // The background is children[0] and the content children[1]; the pair
        // claims whatever the content claims.
        assert_eq!(
            BackgroundLayout.stretch_axis(&[StretchAxis::Both, StretchAxis::Horizontal]),
            StretchAxis::Horizontal
        );
    }
}