sequoia-elements 0.1.1

Sequoia GUI framework — built-in UI elements
Documentation
#![forbid(unsafe_code)]

use sequoia_core::{Color, Edges, Length, Point, Rect};
use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
use sequoia_layout::LayoutStyle;
use sequoia_reactive::Signal;
use sequoia_style::VisualState;
use sequoia_view::{EventCtx, Pad2D as PadMap, PaintCtx, Painter, Unit, View};

pub const KNOB_R: f32 = 7.0;

pub const HEIGHT: f32 = 198.0;

#[derive(Clone)]
pub struct SvPad {
    sat: Signal<f32>,
    val: Signal<f32>,
    hue: Signal<f32>,
    knob_radius: f32,
    style: LayoutStyle,
    margin: Edges,
}

impl SvPad {
    pub fn new(sat: Signal<f32>, val: Signal<f32>, hue: Signal<f32>) -> Self {
        Self {
            sat,
            val,
            hue,
            knob_radius: KNOB_R,
            style: LayoutStyle {
                width: Length::Fill(1.0),
                height: Length::Px(HEIGHT),
                ..LayoutStyle::default()
            },
            margin: Edges::ZERO,
        }
    }

    pub fn knob_radius(mut self, r: f32) -> Self {
        self.knob_radius = r.clamp(3.0, 16.0);
        self
    }

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

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

    pub fn flex(mut self, weight: f32) -> Self {
        self.style = self.style.flex(weight);
        self
    }

    pub fn expand(self) -> Self {
        self.flex(1.0)
    }

    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
    }

    fn apply_at(&self, rect: Rect, pos: Point) {
        let (s, v) = PadMap::new(rect, 0.0).sv_of(pos);
        self.sat.set(s);
        self.val.set(v);
    }

    fn knob_center(&self, rect: Rect) -> Point {
        PadMap::new(rect, 0.0).pos_of(
            self.sat.get_untracked().clamp(0.0, 1.0),
            self.val.get_untracked().clamp(0.0, 1.0),
        )
    }

    fn cur_color(&self) -> Color {
        Color::from_hsva(
            self.hue.get_untracked() / 360.0,
            self.sat.get_untracked().clamp(0.0, 1.0),
            self.val.get_untracked().clamp(0.0, 1.0),
            1.0,
        )
    }
}

impl View for SvPad {
    fn body(&self) -> impl View + 'static {
        let _ = self.sat.get();
        let _ = self.val.get();
        let _ = self.hue.get();
        Unit
    }

    fn is_leaf(&self) -> bool {
        true
    }

    fn focusable(&self) -> bool {
        false
    }

    fn captures_pointer(&self) -> bool {
        true
    }

    fn has_visual_state(&self) -> bool {
        true
    }

    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
        CursorIcon::Crosshair
    }

    fn layout_style(&self) -> LayoutStyle {
        let mut s = self.style;
        s.margin = self.margin;
        s
    }

    fn clip_self(&self) -> bool {
        false
    }

    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
        if ctx.phase != EventPhase::Target {
            return;
        }
        match event {
            Event::Pointer(PointerEvent::Down {
                button: PointerButton::Left,
                pos,
                ..
            }) => {
                self.apply_at(ctx.rect, *pos);
                ctx.capture_pointer();
                ctx.handled();
            }
            Event::Pointer(PointerEvent::Move { pos, .. }) if ctx.state.pressed => {
                self.apply_at(ctx.rect, *pos);
                ctx.handled();
            }
            _ => {}
        }
    }

    fn paint_self(&self, ctx: &mut PaintCtx) {
        let mut g = Painter::new(ctx);
        let rect = g.rect();
        if rect.is_empty() {
            return;
        }
        let overflow = rect.inflate(self.knob_radius, self.knob_radius);
        g.clipped(overflow, |g| {
            g.sv_field(rect, self.hue.get_untracked() / 360.0);
            g.knob(self.knob_center(rect), self.knob_radius, self.cur_color());
        });
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn defaults_fill_width_fixed_height() {
        let p = SvPad::new(
            Signal::new(0.5f32),
            Signal::new(0.5f32),
            Signal::new(200.0f32),
        );
        assert_eq!(p.layout_style().width, Length::Fill(1.0));
        assert_eq!(p.layout_style().height, Length::Px(HEIGHT));
        assert!(!p.clip_self());
    }

    #[test]
    fn drag_clamps_to_corners() {
        let (s, v, h) = (
            Signal::new(0.5f32),
            Signal::new(0.5f32),
            Signal::new(0.0f32),
        );
        let p = SvPad::new(s.clone(), v.clone(), h);
        let rect = Rect::from_xywh(0.0, 0.0, 200.0, 200.0);
        p.apply_at(rect, Point::new(-50.0, -50.0));
        assert_eq!((s.get_untracked(), v.get_untracked()), (0.0, 1.0));
        p.apply_at(rect, Point::new(500.0, 500.0));
        assert_eq!((s.get_untracked(), v.get_untracked()), (1.0, 0.0));
    }
}