sequoia-elements 0.1.2

Sequoia GUI framework — built-in UI elements
Documentation
use std::cell::Cell;
use std::rc::Rc;

use sequoia_core::{Color, Edges, Length, Point, Rect, Size};
use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
use sequoia_layout::{Axis, LayoutCtx, LayoutStyle};
use sequoia_reactive::Signal;
use sequoia_style::{ResizeHandleStyle, VisualState};
use sequoia_view::{EventCtx, PaintCtx, Unit, View};

const DEFAULT_HIT: f32 = 7.0;

const LINE_IDLE: f32 = 1.0;

const LINE_ACTIVE: f32 = 2.0;

const IDLE_COLOR: Color = Color::rgb8(0x75, 0x75, 0x75);

const ACTIVE_COLOR: Color = Color::rgb8(0x0D, 0x99, 0xFF);

#[derive(Clone, Copy)]
struct DragState {
    origin: f32,
    value: f32,
}

#[derive(Clone)]
pub struct ResizeHandle {
    value: Signal<f32>,
    min: f32,
    max: f32,
    hit: f32,
    axis: Axis,
    invert: bool,
    gutter: Color,
    style: Option<ResizeHandleStyle>,
    idle: Option<Color>,
    active: Option<Color>,
    idle_width: Option<f32>,
    active_width: Option<f32>,
    drag: Rc<Cell<Option<DragState>>>,
    margin: Edges,
}

impl ResizeHandle {
    pub fn new(value: Signal<f32>) -> Self {
        Self {
            value,
            min: 0.0,
            max: f32::INFINITY,
            hit: DEFAULT_HIT,
            axis: Axis::Horizontal,
            invert: false,
            gutter: Color::TRANSPARENT,
            style: None,
            idle: None,
            active: None,
            idle_width: None,
            active_width: None,
            drag: Rc::new(Cell::new(None)),
            margin: Edges::ZERO,
        }
    }

    pub fn min(mut self, min: f32) -> Self {
        self.min = if min.is_finite() { min.max(0.0) } else { 0.0 };
        self
    }

    pub fn max(mut self, max: f32) -> Self {
        self.max = if max.is_nan() {
            f32::INFINITY
        } else {
            max.max(0.0)
        };
        self
    }

    pub fn hit_width(mut self, hit: f32) -> Self {
        self.hit = if hit.is_finite() {
            hit.max(3.0)
        } else {
            DEFAULT_HIT
        };
        self
    }

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

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

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

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

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

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

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

    pub fn active_width(mut self, width: f32) -> Self {
        self.active_width = Some(width);
        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
    }

    fn resolved_gutter(&self) -> Color {
        if self.gutter != Color::TRANSPARENT {
            self.gutter
        } else {
            self.style
                .unwrap_or_default()
                .gutter
                .unwrap_or(Color::TRANSPARENT)
        }
    }

    fn resolved_idle(&self) -> Color {
        let style = self.style.unwrap_or_default();
        self.idle.or(style.idle).unwrap_or(IDLE_COLOR)
    }

    fn resolved_active(&self) -> Color {
        let style = self.style.unwrap_or_default();
        self.active.or(style.active).unwrap_or(ACTIVE_COLOR)
    }

    fn resolved_idle_width(&self) -> f32 {
        let style = self.style.unwrap_or_default();
        self.idle_width.or(style.idle_width).unwrap_or(LINE_IDLE)
    }

    fn resolved_active_width(&self) -> f32 {
        let style = self.style.unwrap_or_default();
        self.active_width
            .or(style.active_width)
            .unwrap_or(LINE_ACTIVE)
    }

    fn clamped(&self, v: f32) -> f32 {
        v.clamp(self.min, self.max)
    }

    fn axis_pos(&self, pos: &Point) -> f32 {
        match self.axis {
            Axis::Horizontal => pos.x,
            Axis::Vertical => pos.y,
        }
    }
}

impl View for ResizeHandle {
    fn reuse_state(&self, old: &Self) {
        self.drag.set(old.drag.get());
    }

    fn body(&self) -> impl View + 'static {
        Unit
    }

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

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

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

    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
        match self.axis {
            Axis::Horizontal => CursorIcon::ColResize,
            Axis::Vertical => CursorIcon::RowResize,
        }
    }

    fn layout_style(&self) -> LayoutStyle {
        match self.axis {
            Axis::Horizontal => LayoutStyle {
                width: Length::Px(self.hit),
                height: Length::Fill(1.0),
                margin: self.margin,
                ..LayoutStyle::default()
            },
            Axis::Vertical => LayoutStyle {
                width: Length::Fill(1.0),
                height: Length::Px(self.hit),
                margin: self.margin,
                ..LayoutStyle::default()
            },
        }
    }

    fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
        Some(match self.axis {
            Axis::Horizontal => Size::new(self.hit, 0.0),
            Axis::Vertical => Size::new(0.0, self.hit),
        })
    }

    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,
                ..
            }) => {
                let p = self.axis_pos(pos);
                self.drag.set(Some(DragState {
                    origin: p,
                    value: self.value.get_untracked(),
                }));
                ctx.capture_pointer();
                ctx.handled();
            }
            Event::Pointer(PointerEvent::Move { pos, .. }) => {
                if let Some(st) = self.drag.get() {
                    let p = self.axis_pos(pos);
                    let delta = if self.invert {
                        st.origin - p
                    } else {
                        p - st.origin
                    };
                    self.value.set(self.clamped(st.value + delta));
                    ctx.handled();
                }
            }
            Event::Pointer(PointerEvent::Up {
                button: PointerButton::Left,
                ..
            }) => {
                self.drag.set(None);
            }
            Event::Pointer(PointerEvent::Cancel) => {
                self.drag.set(None);
            }
            _ => {}
        }
    }

    fn paint_self(&self, ctx: &mut PaintCtx) {
        let r = ctx.rect;
        if r.is_empty() {
            return;
        }

        let gutter = self.resolved_gutter();
        if gutter.a > 0.0 {
            ctx.quad(r, gutter, 0.0);
        }
        let active = ctx.state.hovered || ctx.state.pressed || self.drag.get().is_some();
        let (w, c) = if active {
            (self.resolved_active_width(), self.resolved_active())
        } else {
            (self.resolved_idle_width(), self.resolved_idle())
        };
        match self.axis {
            Axis::Horizontal => {
                let cx = r.left() + r.size.width * 0.5;
                ctx.quad(
                    Rect::from_xywh(cx - w * 0.5, r.top(), w, r.size.height),
                    c,
                    0.0,
                );
            }
            Axis::Vertical => {
                let cy = r.top() + r.size.height * 0.5;
                ctx.quad(
                    Rect::from_xywh(r.left(), cy - w * 0.5, r.size.width, w),
                    c,
                    0.0,
                );
            }
        }
    }
}

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