sequoia-elements 0.1.1

Sequoia GUI framework — built-in UI elements
Documentation
use std::cell::RefCell;
use std::collections::HashMap;

use sequoia_core::{Color, Edges, Length};
use sequoia_layout::LayoutStyle;
use sequoia_reactive::Signal;
use sequoia_style::{SheetStyle, Theme};
use sequoia_view::{
    Anchor, AnyView, Dismiss, InputMode, OverlayEntry, OverlayId, OverlayLayer, OverlayOverflow,
    PaintCtx, Placement, Unit, View,
};

thread_local! {
    static ENTRIES: RefCell<HashMap<usize, OverlayId>> = RefCell::new(HashMap::new());
}

fn get_entry(key: usize) -> Option<OverlayId> {
    ENTRIES.with(|m| m.borrow().get(&key).copied())
}

fn insert_entry(key: usize, id: OverlayId) {
    ENTRIES.with(|m| {
        m.borrow_mut().insert(key, id);
    });
}

fn take_entry(key: usize) -> Option<OverlayId> {
    ENTRIES.with(|m| m.borrow_mut().remove(&key))
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum SheetSide {
    #[default]
    Bottom,
    Top,
    Left,
    Right,
}

impl SheetSide {
    fn placement(self) -> Placement {
        match self {
            SheetSide::Bottom => Placement::Bottom,
            SheetSide::Top => Placement::Top,
            SheetSide::Left => Placement::Left,
            SheetSide::Right => Placement::Right,
        }
    }
}

struct SheetPanel {
    child: AnyView,
    width: Length,
    height: Length,
    padding: Edges,
    background: Color,
    border: Color,
    border_width: f32,
    radius: f32,
}

impl View for SheetPanel {
    fn body(&self) -> impl View + 'static {
        Unit
    }
    fn children_override(&self) -> Option<Vec<AnyView>> {
        Some(vec![self.child.clone()])
    }
    fn layout_style(&self) -> LayoutStyle {
        LayoutStyle {
            width: self.width,
            height: self.height,
            padding: self.padding,
            ..LayoutStyle::default()
        }
    }
    fn paint_self(&self, ctx: &mut PaintCtx) {
        if self.border_width > 0.0 && self.border.a > 0.0 {
            ctx.bordered_quad(
                ctx.rect,
                self.background,
                self.radius,
                self.border_width,
                self.border,
            );
        } else {
            ctx.quad(ctx.rect, self.background, self.radius);
        }
    }
}

pub struct Sheet {
    open: Signal<bool>,
    child: Option<AnyView>,
    side: SheetSide,
    width: Length,
    height: Length,
    padding: Edges,
    style: Option<SheetStyle>,
    background: Option<Color>,
    border: Option<Color>,
    border_width: Option<f32>,
    radius: Option<f32>,
    dismiss: Dismiss,
    margin: Edges,
}

impl Sheet {
    pub fn new(open: Signal<bool>) -> Self {
        Self {
            open,
            child: None,
            side: SheetSide::Bottom,
            width: Length::Fit,
            height: Length::Fit,
            padding: Edges::ZERO,
            style: None,
            background: None,
            border: None,
            border_width: None,
            radius: None,
            dismiss: Dismiss::OutsideClick,
            margin: Edges::ZERO,
        }
    }

    pub fn child<V: View + 'static>(mut self, child: V) -> Self {
        self.child = Some(AnyView::new(child));
        self
    }

    pub fn child_if<V: View + 'static>(mut self, cond: bool, make: impl FnOnce() -> V) -> Self {
        if cond {
            self.child = Some(AnyView::new(make()));
        }
        self
    }

    pub fn child_opt<V: View + 'static>(mut self, child: Option<V>) -> Self {
        if let Some(v) = child {
            self.child = Some(AnyView::new(v));
        }
        self
    }

    pub fn side(mut self, side: SheetSide) -> Self {
        self.side = side;
        self
    }

    pub fn bottom(self) -> Self {
        self.side(SheetSide::Bottom)
    }

    pub fn top(self) -> Self {
        self.side(SheetSide::Top)
    }

    pub fn left(self) -> Self {
        self.side(SheetSide::Left)
    }

    pub fn right(self) -> Self {
        self.side(SheetSide::Right)
    }

    pub fn width(mut self, width: impl Into<Length>) -> Self {
        self.width = width.into();
        self
    }

    pub fn height(mut self, height: impl Into<Length>) -> Self {
        self.height = height.into();
        self
    }

    pub fn size(mut self, width: impl Into<Length>, height: impl Into<Length>) -> Self {
        self.width = width.into();
        self.height = height.into();
        self
    }

    pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
        self.padding = padding.into();
        self
    }

    pub fn pad(self, padding: impl Into<Edges>) -> Self {
        self.padding(padding)
    }

    pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
        self.padding = Edges::xy(x, y);
        self
    }

    pub fn style(mut self, style: SheetStyle) -> Self {
        self.style = Some(style);
        self
    }

    pub fn background(mut self, color: Color) -> Self {
        self.background = Some(color);
        self
    }

    pub fn border_color(mut self, color: Color) -> Self {
        self.border = Some(color);
        self
    }

    pub fn border_width(mut self, width: f32) -> Self {
        self.border_width = Some(width.max(0.0));
        self
    }

    pub fn border(mut self, width: f32, color: Color) -> Self {
        self.border_width = Some(width.max(0.0));
        self.border = Some(color);
        self
    }

    pub fn radius(mut self, radius: f32) -> Self {
        self.radius = Some(radius.max(0.0));
        self
    }

    pub fn dismiss(mut self, dismiss: Dismiss) -> Self {
        self.dismiss = dismiss;
        self
    }

    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
        let m = margin.into();
        self.margin = Edges::only(
            sanitize_margin(m.left),
            sanitize_margin(m.top),
            sanitize_margin(m.right),
            sanitize_margin(m.bottom),
        );
        self
    }

    pub fn margin_xy(self, x: f32, y: f32) -> Self {
        self.margin(Edges::xy(x, y))
    }

    pub fn margin_x(mut self, x: f32) -> Self {
        let x = sanitize_margin(x);
        self.margin.left = x;
        self.margin.right = x;
        self
    }

    pub fn margin_y(mut self, y: f32) -> Self {
        let y = sanitize_margin(y);
        self.margin.top = y;
        self.margin.bottom = y;
        self
    }

    pub fn margin_left(mut self, v: f32) -> Self {
        self.margin.left = sanitize_margin(v);
        self
    }

    pub fn margin_right(mut self, v: f32) -> Self {
        self.margin.right = sanitize_margin(v);
        self
    }

    pub fn margin_top(mut self, v: f32) -> Self {
        self.margin.top = sanitize_margin(v);
        self
    }

    pub fn margin_bottom(mut self, v: f32) -> Self {
        self.margin.bottom = sanitize_margin(v);
        self
    }

    pub fn is_open(&self) -> bool {
        self.open.get_untracked()
    }

    pub fn open(&self) {
        self.open.set(true);
    }

    pub fn close(&self) {
        self.open.set(false);
    }

    pub fn toggle(&self) {
        self.open.set(!self.open.get_untracked());
    }

    fn key(&self) -> usize {
        self.open.id()
    }

    fn sync_overlay(&self, ctx: &mut PaintCtx, theme: &Theme) {
        let key = self.key();
        if !self.open.get_untracked() {
            if let Some(id) = take_entry(key) {
                ctx.hide_overlay(id);
            }
            return;
        }
        if let Some(id) = get_entry(key) {
            if !ctx.has_overlay(id) {
                take_entry(key);
                self.open.set(false);
            }
            return;
        }
        let Some(child) = self.child.clone() else {
            self.open.set(false);
            return;
        };
        let style = self.style.unwrap_or_default();
        let panel = SheetPanel {
            child,
            width: self.width,
            height: self.height,
            padding: self.padding,
            background: self
                .background
                .or(style.background)
                .unwrap_or(theme.surface),
            border: self.border.or(style.border).unwrap_or(theme.border),
            border_width: self
                .border_width
                .or(style.border_width)
                .unwrap_or(0.0)
                .max(0.0),
            radius: self.radius.or(style.radius).unwrap_or(theme.radius),
        };
        let id = ctx.show_overlay(
            OverlayEntry::new(panel)
                .anchor(Anchor::Viewport)
                .placement(self.side.placement())
                .layer(OverlayLayer::Modal)
                .input_mode(InputMode::Modal)
                .dismiss(self.dismiss)
                .overflow(OverlayOverflow::Clamp),
        );
        insert_entry(key, id);
    }
}

impl View for Sheet {
    fn body(&self) -> impl View + 'static {
        Unit
    }
    fn is_leaf(&self) -> bool {
        true
    }
    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
        let _ = self.open.get();
        Some(Vec::new())
    }
    fn layout_style(&self) -> LayoutStyle {
        LayoutStyle {
            width: Length::Fit,
            height: Length::Fit,
            margin: self.margin,
            ..LayoutStyle::default()
        }
    }
    fn on_unmount(&self) {
        take_entry(self.key());
    }
    fn paint_self(&self, ctx: &mut PaintCtx) {
        let theme = ctx.theme();
        self.sync_overlay(ctx, &theme);
    }
}

fn sanitize_margin(v: f32) -> f32 {
    if v.is_finite() { v.max(0.0) } else { 0.0 }
}