sequoia-elements 0.1.0

Sequoia GUI framework — built-in UI elements
Documentation
use sequoia_core::{Color, Edges, Length, Point, Rect, Size};
use sequoia_input::{CursorIcon, Event, EventPhase, Key, PointerButton, PointerEvent};
use sequoia_layout::{LayoutCtx, LayoutStyle};
use sequoia_reactive::Signal;
use sequoia_style::{SwitchStyle, VisualState};
use sequoia_view::{AnyView, EventCtx, PaintCtx, Unit, View};

const TRACK_W: f32 = 40.0;
const TRACK_H: f32 = 22.0;
const GAP: f32 = 8.0;
const BORDER_W: f32 = 1.0;

pub struct Switch {
    checked: Signal<bool>,
    label: Option<String>,
    style: Option<SwitchStyle>,
    track_on: Option<Color>,
    track_off: Option<Color>,
    track_disabled: Option<Color>,
    thumb: Option<Color>,
    thumb_on: Option<Color>,
    thumb_disabled: Option<Color>,
    border: Option<Color>,
    border_on: Option<Color>,
    text_color: Option<Color>,
    font_size: Option<f32>,
    track_width: Option<f32>,
    track_height: Option<f32>,
    thumb_size: Option<f32>,
    disabled: bool,
    on_toggle: Option<Box<dyn Fn(bool)>>,
    margin: Edges,
}

impl Switch {
    pub fn new(checked: Signal<bool>) -> Self {
        Self {
            checked,
            label: None,
            style: None,
            track_on: None,
            track_off: None,
            track_disabled: None,
            thumb: None,
            thumb_on: None,
            thumb_disabled: None,
            border: None,
            border_on: None,
            text_color: None,
            font_size: None,
            track_width: None,
            track_height: None,
            thumb_size: None,
            disabled: false,
            on_toggle: None,
            margin: Edges::ZERO,
        }
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

    pub fn track_height(mut self, height: f32) -> Self {
        self.track_height = Some(height.max(8.0));
        self
    }

    pub fn thumb_size(mut self, size: f32) -> Self {
        self.thumb_size = Some(size.max(4.0));
        self
    }

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

    pub fn on_toggle(mut self, f: impl Fn(bool) + 'static) -> Self {
        self.on_toggle = Some(Box::new(f));
        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_on(&self) -> bool {
        self.checked.get_untracked()
    }

    fn resolved_track_size(&self) -> (f32, f32) {
        let style = self.style.unwrap_or_default();
        let w = self.track_width.or(style.width).unwrap_or(TRACK_W).max(8.0);
        let h = self
            .track_height
            .or(style.height)
            .unwrap_or(TRACK_H)
            .max(8.0);
        (w, h)
    }

    fn resolved_thumb_d(&self, track_h: f32) -> f32 {
        let style = self.style.unwrap_or_default();
        self.thumb_size
            .or(style.thumb_size)
            .unwrap_or((track_h - 4.0).max(4.0))
            .clamp(4.0, track_h.max(4.0))
    }

    fn toggle(&self) {
        if self.disabled {
            return;
        }
        let next = !self.checked.get_untracked();
        self.checked.set(next);
        if let Some(f) = &self.on_toggle {
            f(next);
        }
    }
}

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

impl View for Switch {
    fn body(&self) -> impl View + 'static {
        Unit
    }
    fn is_leaf(&self) -> bool {
        true
    }
    fn focusable(&self) -> bool {
        !self.disabled
    }
    fn has_visual_state(&self) -> bool {
        true
    }
    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
        if self.disabled {
            CursorIcon::Default
        } else {
            CursorIcon::Pointer
        }
    }
    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
        let _ = self.checked.get();
        Some(Vec::new())
    }
    fn layout_style(&self) -> LayoutStyle {
        LayoutStyle {
            width: Length::Fit,
            height: Length::Fit,
            margin: self.margin,
            ..LayoutStyle::default()
        }
    }
    fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
        let style = self.style.unwrap_or_default();
        let size = self.font_size.or(style.font_size).unwrap_or(14.0);
        let (track_w, track_h) = self.resolved_track_size();
        let label_w = self
            .label
            .as_deref()
            .map(|l| ctx.text_width(l, size))
            .unwrap_or(0.0);
        let gap = if self.label.is_some() { GAP } else { 0.0 };
        let h = track_h.max(LayoutCtx::text_line_height(size));
        Some(Size::new(track_w + gap + label_w, h))
    }
    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
        if self.disabled || ctx.phase != EventPhase::Target {
            return;
        }
        if let Event::Pointer(PointerEvent::Up {
            button: PointerButton::Left,
            ..
        }) = event
        {
            if ctx.state.pressed && ctx.hover_self {
                self.toggle();
                ctx.handled();
            }
            return;
        }
        if let Event::Key(key) = event
            && ctx.state.focused
            && (key.key == Key::ENTER || key.key == Key::SPACE)
        {
            self.toggle();
            ctx.handled();
        }
    }
    fn paint_self(&self, ctx: &mut PaintCtx) {
        let theme = ctx.theme();
        let mut vs = ctx.state;
        vs.disabled = self.disabled;
        vs.selected = self.checked.get_untracked();
        let style = self.style.unwrap_or_default();
        let (track_w, track_h) = self.resolved_track_size();
        let thumb_d = self.resolved_thumb_d(track_h);
        let thumb_r = thumb_d * 0.5;
        let pad = ((track_h - thumb_d) * 0.5).max(1.0);

        let rect = ctx.rect;
        let track_y = rect.top() + (rect.size.height - track_h) * 0.5;
        let track_rect = Rect::from_xywh(rect.left(), track_y, track_w, track_h);
        let radius = track_h * 0.5;

        let track = if vs.disabled {
            self.track_disabled
                .or(style.track_disabled)
                .unwrap_or(theme.disabled)
        } else if vs.selected {
            self.track_on
                .or(style.track_on)
                .unwrap_or(Color::hex(0x3A3A3A))
        } else {
            self.track_off.or(style.track_off).unwrap_or(theme.surface)
        };
        let border = if vs.selected {
            self.border_on
                .or(style.border_on)
                .or(self.border)
                .or(style.border)
        } else {
            self.border.or(style.border)
        };
        let has_border = border.is_some_and(|c| c.a > 0.0);
        if has_border {
            ctx.bordered_quad(track_rect, track, radius, BORDER_W, border.unwrap());
        } else {
            ctx.quad(track_rect, track, radius);
        }

        let thumb = if vs.disabled {
            self.thumb_disabled
                .or(style.thumb_disabled)
                .or(self.thumb)
                .or(style.thumb)
                .unwrap_or(Color::WHITE)
        } else if vs.selected {
            self.thumb_on
                .or(style.thumb_on)
                .or(self.thumb)
                .or(style.thumb)
                .unwrap_or(Color::WHITE)
        } else {
            self.thumb.or(style.thumb).unwrap_or(Color::WHITE)
        };
        let cx = if vs.selected {
            track_rect.right() - pad - thumb_r
        } else {
            track_rect.left() + pad + thumb_r
        };
        let cy = track_rect.top() + track_h * 0.5;
        ctx.disk(Point::new(cx, cy), thumb_r, thumb);

        if let Some(label) = &self.label {
            let size = self
                .font_size
                .or(style.font_size)
                .unwrap_or(theme.font_size);
            let text = self.text_color.or(style.text).unwrap_or(theme.text);
            let x = rect.left() + track_w + GAP;
            let y = rect.top() + rect.size.height * 0.5 + size * 0.35;
            ctx.draw_text(label, (x, y), size, text);
        }
    }
}