container.rs raw

//! The Container primitive: a styled box that lays out children via taffy.

use guiduck_scene::Fragment;
use guiduck_scene::geom::{Rect, Size, Stroke};
use guiduck_scene::paint::Brush;

use super::Widget;
use crate::dirty::Dirt;

/// A box with an optional background, rounded corners, and border. All
/// layout behavior (flex/grid, padding, gap, …) comes from the taffy style
/// the container was inserted with.
#[derive(Default)]
pub struct Container {
    background: Option<Brush>,
    corner_radius: f64,
    border: Option<(Brush, f64)>,
    dirt: Dirt,
}

impl Container {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn background(mut self, brush: impl Into<Brush>) -> Self {
        self.set_background(brush);
        self
    }

    pub fn corner_radius(mut self, radius: f64) -> Self {
        self.set_corner_radius(radius);
        self
    }

    pub fn border(mut self, brush: impl Into<Brush>, width: f64) -> Self {
        self.border = Some((brush.into(), width));
        self
    }

    /// The background currently in effect (the builder method of the same
    /// name sets it at construction).
    pub fn current_background(&self) -> Option<&Brush> {
        self.background.as_ref()
    }

    /// Give the box a background. "No background" is the state a fresh
    /// container is already in, so there is nothing here to unset: the
    /// property a `.gdc` writes is a brush, and this is the setter that takes
    /// one.
    pub fn set_background(&mut self, brush: impl Into<Brush>) {
        let brush = Some(brush.into());
        if self.background == brush {
            return;
        }
        self.background = brush;
        self.dirt.mark_paint();
    }

    pub fn set_corner_radius(&mut self, radius: f64) {
        if self.corner_radius == radius {
            return;
        }
        self.corner_radius = radius;
        self.dirt.mark_paint();
    }

    pub fn set_border(&mut self, border: Option<(Brush, f64)>) {
        if self.border == border {
            return;
        }
        self.border = border;
        self.dirt.mark_paint();
    }
}

impl Widget for Container {
    fn paint(&mut self, fragment: &mut Fragment, size: Size) {
        let bounds = Rect::from_origin_size((0.0, 0.0), size);
        if let Some(background) = &self.background {
            fragment.fill(
                bounds.to_rounded_rect(self.corner_radius),
                background.clone(),
            );
        }
        if let Some((brush, width)) = &self.border {
            // Stroke centered on an inset path so the border stays inside
            // the widget's bounds.
            let inset = bounds.inset(-width / 2.0);
            fragment.stroke(
                inset.to_rounded_rect((self.corner_radius - width / 2.0).max(0.0)),
                brush.clone(),
                Stroke::new(*width),
            );
        }
    }

    fn role(&self) -> accesskit::Role {
        accesskit::Role::GenericContainer
    }

    fn take_dirt(&mut self) -> Dirt {
        std::mem::take(&mut self.dirt)
    }

    fn type_name(&self) -> &'static str {
        "container"
    }

    fn apply_style(&mut self, style: &crate::style::ComputedStyle) {
        if let Some(brush) = style.brush("background") {
            self.set_background(brush.clone());
        }
        if let Some(radius) = style.number("corner-radius") {
            self.set_corner_radius(radius);
        }
    }
}