frus-widgets 0.5.0

frus — declarative widget tree
Documentation
//! [`Positioned`]: places a layer of a [`crate::Stack`] against the stack's own edges,
//! instead of filling it.
//!
//! A badge on a corner, a caption over an image, a button floating above content — all of
//! them are a stack with one layer pinned somewhere. Without this a layer can only fill
//! the box, and the only way to put something in a corner was to give it a transparent
//! wrapper the size of the stack and align inside it.
//!
//! Each edge is optional, and what is given decides both the **size** and the **place**,
//! exactly as in the reference:
//!
//! - `left` **and** `right` → the width is what is between them;
//! - one of them plus [`Positioned::width`] → that width, at that edge;
//! - neither → the child's own width, placed by the stack's alignment.
//!
//! The vertical axis works the same way with `top`, `bottom` and [`Positioned::height`].
//! Giving all three of a triple is a contradiction, and the extra one is ignored: the two
//! edges win, since they are what says where the box is.
//!
//! ```ignore
//! Stack::new()
//!     .layer(photo)
//!     .layer(Positioned::new(badge).top(8.0).right(8.0))
//! ```

use frus_layout::Style;

use crate::widget::Widget;

/// What a [`Positioned`] pins, in logical pixels from the stack's edges. `None` on an
/// edge means "not pinned there".
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Positioning {
    /// The distance from the stack's left edge, or `None` to leave that edge free.
    pub left: Option<f32>,
    /// The distance from the stack's top edge, or `None` to leave that edge free.
    pub top: Option<f32>,
    /// The distance from the stack's right edge, or `None` to leave that edge free.
    pub right: Option<f32>,
    /// The distance from the stack's bottom edge, or `None` to leave that edge free.
    pub bottom: Option<f32>,
    /// A fixed width, or `None` to take the one the edges and the content leave.
    pub width: Option<f32>,
    /// A fixed height, or `None` to take the one the edges and the content leave.
    pub height: Option<f32>,
    /// The distance from the stack's **start** edge — the left in a left-to-right script,
    /// the right in a right-to-left one — or `None` (milestone 589).
    pub start: Option<f32>,
    /// The distance from the stack's **end** edge, or `None`.
    pub end: Option<f32>,
}

impl Positioning {
    /// The same pins with `start` and `end` made physical for the reading direction: the
    /// start is the left in a left-to-right script and the right in a right-to-left one. An
    /// edge given physically as well keeps its own value.
    pub fn for_direction(self, rtl: bool) -> Self {
        let (from_left, from_right) = if rtl {
            (self.end, self.start)
        } else {
            (self.start, self.end)
        };
        Self {
            left: self.left.or(from_left),
            right: self.right.or(from_right),
            start: None,
            end: None,
            ..self
        }
    }

    /// The width this asks for in a stack `available` wide, and `None` when it asks for
    /// the child's own.
    pub fn resolved_width(&self, available: f32) -> Option<f32> {
        match (self.left, self.right) {
            (Some(l), Some(r)) => Some((available - l - r).max(0.0)),
            _ => self.width,
        }
    }

    /// The height this asks for in a stack `available` tall, and `None` when it asks for
    /// the child's own.
    pub fn resolved_height(&self, available: f32) -> Option<f32> {
        match (self.top, self.bottom) {
            (Some(t), Some(b)) => Some((available - t - b).max(0.0)),
            _ => self.height,
        }
    }

    /// Where the box goes on one axis, given the stack's extent, the child's own and the
    /// share of the free space the stack's alignment asks for.
    ///
    /// The near edge wins if it is pinned; failing that the far edge places the box by
    /// its own size; failing both, the stack aligns it.
    pub fn place(
        near: Option<f32>,
        far: Option<f32>,
        available: f32,
        own: f32,
        fraction: f32,
    ) -> f32 {
        match (near, far) {
            (Some(n), _) => n,
            (None, Some(f)) => available - f - own,
            (None, None) => (available - own).max(0.0) * fraction,
        }
    }
}

/// A layer pinned against a [`crate::Stack`]'s edges. See the module documentation.
pub struct Positioned<Msg = crate::callback::Callback> {
    inner: Box<dyn Widget<Msg>>,
    spec: Positioning,
}

impl<Msg> Positioned<Msg> {
    /// Wraps a layer, pinned nowhere yet — which on its own is the stack's alignment at
    /// the child's natural size.
    pub fn new(inner: impl Widget<Msg> + 'static) -> Self {
        Self {
            inner: Box::new(inner),
            spec: Positioning::default(),
        }
    }

    /// Distance from the stack's left edge — the left in either reading direction; see
    /// [`start`](Self::start) for one that follows the script.
    pub fn left(mut self, px: f32) -> Self {
        self.spec.left = Some(px);
        self
    }

    /// Distance from the stack's top edge.
    pub fn top(mut self, px: f32) -> Self {
        self.spec.top = Some(px);
        self
    }

    /// Distance from the stack's right edge.
    pub fn right(mut self, px: f32) -> Self {
        self.spec.right = Some(px);
        self
    }

    /// Distance from the stack's bottom edge.
    pub fn bottom(mut self, px: f32) -> Self {
        self.spec.bottom = Some(px);
        self
    }

    /// Distance from the stack's **start** edge: the left in a left-to-right script, the
    /// right in a right-to-left one (milestone 589). A `left` or a `right` set as well wins
    /// on its own side.
    pub fn start(mut self, px: f32) -> Self {
        self.spec.start = Some(px);
        self
    }

    /// Distance from the stack's **end** edge: the right in a left-to-right script, the left
    /// in a right-to-left one.
    pub fn end(mut self, px: f32) -> Self {
        self.spec.end = Some(px);
        self
    }

    /// An explicit width, used when only one horizontal edge is pinned.
    pub fn width(mut self, px: f32) -> Self {
        self.spec.width = Some(px);
        self
    }

    /// An explicit height, used when only one vertical edge is pinned.
    pub fn height(mut self, px: f32) -> Self {
        self.spec.height = Some(px);
        self
    }

    /// Pins all four edges: the layer fills the stack inset by `px` on every side.
    pub fn inset(self, px: f32) -> Self {
        self.left(px).top(px).right(px).bottom(px)
    }

    /// **A layer the size of the stack** — the reference's `Positioned.fill`: every edge
    /// pinned at nought.
    pub fn fill(inner: impl Widget<Msg> + 'static) -> Self {
        Self::new(inner).inset(0.0)
    }

    /// **A layer at `rect`**, in the stack's own coordinates — the reference's
    /// `Positioned.fromRect`: pinned left and top, with that width and height.
    pub fn from_rect(inner: impl Widget<Msg> + 'static, rect: frus_core::Rect) -> Self {
        Self::new(inner)
            .left(rect.x)
            .top(rect.y)
            .width(rect.width)
            .height(rect.height)
    }

    /// **A layer inset from each edge of the stack by its own amount** — the reference's
    /// `Positioned.fromRelativeRect`: `edges` are the distances from the left, top, right
    /// and bottom edges.
    pub fn from_relative_rect(inner: impl Widget<Msg> + 'static, edges: frus_core::Insets) -> Self {
        Self::new(inner)
            .left(edges.left)
            .top(edges.top)
            .right(edges.right)
            .bottom(edges.bottom)
    }

    /// It does not touch the box — the stack reads the pins and lays the layer out
    /// against them, which a style cannot express.
    fn restyle(&self, base: Style) -> Style {
        base
    }
}

crate::transparent::forward_transparent!(Positioned {
    /// Claimed: the pins are the whole of what this wrapper adds.
    fn positioned(&self) -> Option<crate::positioned::Positioning> {
        Some(self.spec)
    }

    /// Forwarded: pinning a widget says nothing about which widget it is.
    fn key(&self) -> Option<u64> {
        self.inner.key()
    }

    /// Forwarded too: a place is not a palette.
    fn theme_override(
        &self,
        inherited: &crate::theme::Theme,
    ) -> Option<Box<crate::theme::Theme>> {
        self.inner.theme_override(inherited)
    }

    /// Forwarded: a wrapper is its child, and a scoped surface is the child's to impose.
    fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
        self.inner.media_override(inherited)
    }
    fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
        self.inner.scaffold_override()
    }
    fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
        self.inner.scroll_behavior_override()
    }
    /// Forwarded: a wrapper around a form is still that form (milestone 512).
    fn autofill_group(&self) -> bool {
        self.inner.autofill_group()
    }
    /// Forwarded: the area's bar is the area's, whatever wraps it.
    fn area_toolbar(
        &self,
        context: crate::ToolbarContext,
    ) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
        self.inner.area_toolbar(context)
    }
});

/// Start and end pins follow the reading direction; left and right do not (milestone 589).
#[cfg(test)]
mod direction_tests {
    use super::*;
    use crate::{build_ui, AnimatedPositioned, Container, Runtime, Stack, Theme};
    use frus_core::{Color, Curve, Primitive, Size};

    const RED: Color = Color::rgb(1.0, 0.0, 0.0);

    fn square() -> Container<()> {
        Container::new().width(20.0).height(20.0).color(RED)
    }

    /// Where the square landed, across a 100-px stack.
    fn x_of(layer: impl Widget<()> + 'static, theme: &Theme) -> f32 {
        let stack = Stack::<()>::new().width(100.0).height(100.0).layer(layer);
        build_ui(&stack, Size::new(100.0, 100.0), &Runtime::default(), theme)
            .scene()
            .primitives()
            .iter()
            .find_map(|p| match p {
                Primitive::Rect { rect, color, .. } if *color == RED => Some(rect.x),
                _ => None,
            })
            .expect("the square")
    }

    /// **A start pin is on the left in left to right and on the right in right to left**;
    /// an end pin the other way round; a left pin on the left in both.
    #[test]
    fn start_and_end_follow_the_script_and_left_does_not() {
        let (ltr, rtl) = (Theme::default(), Theme::default().rtl());
        let start = || Positioned::new(square()).start(10.0).top(0.0);
        assert_eq!(x_of(start(), &ltr), 10.0);
        assert_eq!(x_of(start(), &rtl), 70.0, "10 from the right");
        let end = || Positioned::new(square()).end(10.0).top(0.0);
        assert_eq!(x_of(end(), &ltr), 70.0);
        assert_eq!(x_of(end(), &rtl), 10.0);
        let left = || Positioned::new(square()).left(10.0).top(0.0);
        assert_eq!(x_of(left(), &rtl), 10.0, "a left pin is on the left");
        // A physical pin given as well wins on its own side.
        let both = || Positioned::new(square()).start(30.0).left(10.0).top(0.0);
        assert_eq!(x_of(both(), &ltr), 10.0);
    }

    /// **An animated layer takes start and end too**, and its moving pins carry them.
    #[test]
    fn an_animated_layer_follows_the_script() {
        let rtl = Theme::default().rtl();
        let layer = || {
            AnimatedPositioned::new(0.2, Curve::Linear, square())
                .start(10.0)
                .top(0.0)
        };
        assert_eq!(x_of(layer(), &rtl), 70.0);
        let stack = Stack::<()>::new().width(100.0).height(100.0).layer(layer());
        let mut rt = Runtime::default();
        rt.advance_pins(&stack, 0.0);
        let pinned = (0..8)
            .map(|i| crate::interaction::WidgetId::ROOT.child(i))
            .find_map(|id| rt.anim_pins(id))
            .expect("the layer's pins");
        assert_eq!(pinned.start, Some(10.0));
        assert_eq!(pinned.for_direction(true).right, Some(10.0));
    }
}