fui-rs 0.2.3-alpha2

Retained-mode Rust UI SDK for EffinDOM
Documentation
use crate::controls::{SliderColors, SliderSizing};
use crate::ffi::{Orientation, PositionType, Unit};
use crate::node::{flex_box, FlexBox};
use crate::theme::Theme;
use std::rc::Rc;

fn clamp(value: f32, min: f32, max: f32) -> f32 {
    if value < min {
        return min;
    }
    if value > max {
        return max;
    }
    value
}

fn resolve_thumb_size(metrics: SliderPresenterMetrics) -> f32 {
    if metrics.thumb_size > 1.0 {
        metrics.thumb_size
    } else {
        1.0
    }
}

fn resolve_track_thickness(metrics: SliderPresenterMetrics) -> f32 {
    clamp(metrics.track_thickness, 1.0, resolve_thumb_size(metrics))
}

fn resolve_cross_axis_extra(metrics: SliderPresenterMetrics) -> f32 {
    if metrics.cross_axis_extra > 0.0 {
        metrics.cross_axis_extra
    } else {
        0.0
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SliderPresenterMetrics {
    pub thumb_size: f32,
    pub track_thickness: f32,
    pub cross_axis_extra: f32,
}

impl SliderPresenterMetrics {
    pub const fn new(thumb_size: f32, track_thickness: f32) -> Self {
        Self {
            thumb_size,
            track_thickness,
            cross_axis_extra: 2.0,
        }
    }

    pub const fn with_cross_axis_extra(mut self, cross_axis_extra: f32) -> Self {
        self.cross_axis_extra = cross_axis_extra;
        self
    }

    pub const fn with_values(thumb_size: f32, track_thickness: f32, cross_axis_extra: f32) -> Self {
        Self {
            thumb_size,
            track_thickness,
            cross_axis_extra,
        }
    }
}

const DEFAULT_SLIDER_METRICS: SliderPresenterMetrics = SliderPresenterMetrics::new(18.0, 6.0);

fn resolve_slider_metrics(sizing: Option<SliderSizing>) -> SliderPresenterMetrics {
    let Some(sizing) = sizing else {
        return DEFAULT_SLIDER_METRICS;
    };
    let thumb_size = if sizing.has_thumb_size() {
        sizing.thumb_size_px()
    } else {
        DEFAULT_SLIDER_METRICS.thumb_size
    };
    let track_thickness = if sizing.has_track_thickness() {
        sizing.track_thickness_px()
    } else {
        DEFAULT_SLIDER_METRICS.track_thickness
    };
    if (thumb_size - DEFAULT_SLIDER_METRICS.thumb_size).abs() <= f32::EPSILON
        && (track_thickness - DEFAULT_SLIDER_METRICS.track_thickness).abs() <= f32::EPSILON
    {
        return DEFAULT_SLIDER_METRICS;
    }
    SliderPresenterMetrics::new(thumb_size, track_thickness)
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SliderVisualState {
    pub value: f32,
    pub min: f32,
    pub max: f32,
    pub normalized_value: f32,
    pub orientation: Orientation,
    pub hovered: bool,
    pub dragging: bool,
    pub focused: bool,
    pub enabled: bool,
}

impl SliderVisualState {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        value: f32,
        min: f32,
        max: f32,
        normalized_value: f32,
        orientation: Orientation,
        hovered: bool,
        dragging: bool,
        focused: bool,
        enabled: bool,
    ) -> Self {
        Self {
            value,
            min,
            max,
            normalized_value,
            orientation,
            hovered,
            dragging,
            focused,
            enabled,
        }
    }
}

pub trait SliderPresenter {
    fn root(&self) -> FlexBox;
    fn metrics(&self) -> SliderPresenterMetrics;
    fn layout(&self, state: SliderVisualState, length: f32);
    fn apply(&self, theme: Theme, state: SliderVisualState, colors: Option<SliderColors>);
}

pub trait SliderTemplate {
    fn create(&self, sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter>;
}

#[derive(Clone)]
pub struct DefaultSliderPresenter {
    root: FlexBox,
    metrics: SliderPresenterMetrics,
    track_node: FlexBox,
    fill_node: FlexBox,
    thumb_node: FlexBox,
}

impl DefaultSliderPresenter {
    pub fn new(metrics: SliderPresenterMetrics) -> Self {
        let root = flex_box();
        let track_node = flex_box();
        track_node.position_type(PositionType::Absolute);
        let fill_node = flex_box();
        fill_node.position_type(PositionType::Absolute);
        let thumb_node = flex_box();
        thumb_node
            .position_type(PositionType::Absolute)
            .width(metrics.thumb_size, Unit::Pixel)
            .height(metrics.thumb_size, Unit::Pixel);
        root.child(&track_node).child(&fill_node).child(&thumb_node);
        Self {
            root,
            metrics,
            track_node,
            fill_node,
            thumb_node,
        }
    }
}

impl SliderPresenter for DefaultSliderPresenter {
    fn root(&self) -> FlexBox {
        self.root.clone()
    }

    fn metrics(&self) -> SliderPresenterMetrics {
        self.metrics
    }

    fn layout(&self, state: SliderVisualState, length: f32) {
        let metrics = self.metrics;
        let thumb_size = resolve_thumb_size(metrics);
        let track_thickness = resolve_track_thickness(metrics);
        let cross_axis_extra = resolve_cross_axis_extra(metrics);
        let available = if length > thumb_size {
            length - thumb_size
        } else {
            0.0
        };
        let fraction = clamp(state.normalized_value, 0.0, 1.0);
        let cross_axis_inset = cross_axis_extra * 0.5;
        let track_offset = cross_axis_inset + ((thumb_size - track_thickness) * 0.5);
        if state.orientation == Orientation::Vertical {
            self.root
                .width(thumb_size + cross_axis_extra, Unit::Pixel)
                .height(length, Unit::Pixel);
            self.track_node
                .width(track_thickness, Unit::Pixel)
                .height(available, Unit::Pixel)
                .position(track_offset, thumb_size * 0.5);
            self.fill_node
                .width(track_thickness, Unit::Pixel)
                .height(available * fraction, Unit::Pixel)
                .position(
                    track_offset,
                    thumb_size * 0.5 + (available * (1.0 - fraction)),
                );
            self.thumb_node
                .position(cross_axis_inset, available - (available * fraction));
            return;
        }

        self.root
            .width(length, Unit::Pixel)
            .height(thumb_size + cross_axis_extra, Unit::Pixel);
        self.track_node
            .width(available, Unit::Pixel)
            .height(track_thickness, Unit::Pixel)
            .position(thumb_size * 0.5, track_offset);
        self.fill_node
            .width(available * fraction, Unit::Pixel)
            .height(track_thickness, Unit::Pixel)
            .position(thumb_size * 0.5, track_offset);
        self.thumb_node
            .position(available * fraction, cross_axis_inset);
    }

    fn apply(&self, theme: Theme, state: SliderVisualState, colors: Option<SliderColors>) {
        let accent = if state.dragging {
            theme.colors.accent_pressed
        } else if state.hovered {
            theme.colors.accent_hovered
        } else {
            theme.colors.accent
        };
        let metrics = self.metrics;
        let thumb_size = resolve_thumb_size(metrics);
        let track_thickness = resolve_track_thickness(metrics);
        let track_radius = track_thickness * 0.5;
        self.track_node.corner_radius(track_radius);
        let track_color = colors
            .filter(|colors| colors.has_track())
            .map(|colors| colors.track_color())
            .unwrap_or(theme.colors.scrollbar_track);
        self.track_node.bg_color(track_color);
        self.fill_node.corner_radius(track_radius);
        let fill_color = colors
            .filter(|colors| colors.has_fill())
            .map(|colors| colors.fill_color())
            .unwrap_or(accent);
        self.fill_node.bg_color(fill_color);
        self.thumb_node
            .width(thumb_size, Unit::Pixel)
            .height(thumb_size, Unit::Pixel)
            .corner_radius(thumb_size * 0.5);
        let thumb_color = colors
            .filter(|colors| colors.has_thumb())
            .map(|colors| colors.thumb_color())
            .unwrap_or(accent);
        self.thumb_node
            .bg_color(thumb_color)
            .border(1.0, theme.colors.surface);
    }
}

#[derive(Clone, Copy, Debug, Default)]
pub struct DefaultSliderTemplate;

impl SliderTemplate for DefaultSliderTemplate {
    fn create(&self, sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter> {
        create_default_slider_presenter(sizing)
    }
}

pub const DEFAULT_SLIDER_TEMPLATE: DefaultSliderTemplate = DefaultSliderTemplate;

pub fn create_default_slider_presenter(sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter> {
    Rc::new(DefaultSliderPresenter::new(resolve_slider_metrics(sizing)))
}