fui-rs 0.2.3-alpha1

Retained-mode Rust UI SDK for EffinDOM
Documentation
use super::pressable_indicator_presenter::{
    PressableIndicatorMetrics, PressableIndicatorPresenter, PressableIndicatorVisualState,
};
use crate::controls::{LabeledControlColors, LabeledControlSizing};
use crate::ffi::{AlignItems, JustifyContent, SemanticCheckedState, Unit};
use crate::node::{flex_box, svg, BoxStyleSurface, FlexBox, SvgNode};
use crate::theme::Theme;
use std::rc::Rc;

const CHECKBOX_CHECK_SVG_URL: &str = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 14 14'><path d='M2.25 7.15 5.35 10.25 11.75 3.85' fill='none' stroke='%23000000' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/></svg>";

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CheckboxIndicatorMetrics {
    pub indicator_size: f32,
    pub corner_radius: f32,
    pub check_mark_size: f32,
}

impl CheckboxIndicatorMetrics {
    pub const fn new(indicator_size: f32, corner_radius: f32, check_mark_size: f32) -> Self {
        Self {
            indicator_size,
            corner_radius,
            check_mark_size,
        }
    }
}

const DEFAULT_CHECKBOX_METRICS: CheckboxIndicatorMetrics =
    CheckboxIndicatorMetrics::new(20.0, 4.0, 16.0);

pub fn resolve_checkbox_metrics(sizing: Option<LabeledControlSizing>) -> CheckboxIndicatorMetrics {
    let Some(sizing) = sizing else {
        return DEFAULT_CHECKBOX_METRICS;
    };
    if !sizing.has_indicator_size() {
        return DEFAULT_CHECKBOX_METRICS;
    }
    let indicator_size = sizing.indicator_size_px();
    CheckboxIndicatorMetrics::new(indicator_size, indicator_size * 0.2, indicator_size * 0.8)
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CheckboxIndicatorVisualState {
    pub checked_state: SemanticCheckedState,
    pub hovered: bool,
    pub pressed: bool,
    pub focused: bool,
    pub enabled: bool,
}

impl CheckboxIndicatorVisualState {
    pub fn new(
        checked_state: SemanticCheckedState,
        hovered: bool,
        pressed: bool,
        focused: bool,
        enabled: bool,
    ) -> Self {
        Self {
            checked_state,
            hovered,
            pressed,
            focused,
            enabled,
        }
    }

    pub fn pressable_state(&self) -> PressableIndicatorVisualState {
        PressableIndicatorVisualState {
            hovered: self.hovered,
            pressed: self.pressed,
            focused: self.focused,
            enabled: self.enabled,
        }
    }
}

pub trait CheckboxIndicatorPresenter: PressableIndicatorPresenter {
    fn apply(
        &self,
        theme: Theme,
        state: CheckboxIndicatorVisualState,
        colors: Option<LabeledControlColors>,
    );
}

pub trait CheckboxIndicatorTemplate {
    fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn CheckboxIndicatorPresenter>;
}

#[derive(Clone)]
pub struct DefaultCheckboxIndicatorPresenter {
    root: FlexBox,
    metrics: CheckboxIndicatorMetrics,
    mark_node: SvgNode,
}

impl DefaultCheckboxIndicatorPresenter {
    pub fn new(metrics: CheckboxIndicatorMetrics) -> Self {
        let root = flex_box();
        root.width(metrics.indicator_size, Unit::Pixel)
            .height(metrics.indicator_size, Unit::Pixel)
            .align_items(AlignItems::Center)
            .justify_content(JustifyContent::Center);
        let mark_host = flex_box();
        mark_host
            .fill_size()
            .align_items(AlignItems::Center)
            .justify_content(JustifyContent::Center);
        let mark_node = svg(0);
        mark_node
            .width(metrics.check_mark_size, Unit::Pixel)
            .height(metrics.check_mark_size, Unit::Pixel);
        mark_host.child(&mark_node);
        root.child(&mark_host);
        Self {
            root,
            metrics,
            mark_node,
        }
    }
}

impl PressableIndicatorPresenter for DefaultCheckboxIndicatorPresenter {
    fn root(&self) -> FlexBox {
        self.root.clone()
    }

    fn metrics(&self) -> PressableIndicatorMetrics {
        PressableIndicatorMetrics::new(self.metrics.indicator_size, self.metrics.indicator_size)
    }
}

impl CheckboxIndicatorPresenter for DefaultCheckboxIndicatorPresenter {
    fn apply(
        &self,
        theme: Theme,
        state: CheckboxIndicatorVisualState,
        colors: Option<LabeledControlColors>,
    ) {
        let metrics = self.metrics;
        let accent = colors
            .filter(|colors| colors.has_accent())
            .map(|colors| colors.accent_color())
            .unwrap_or_else(|| {
                if state.pressed {
                    theme.colors.accent_pressed
                } else if state.hovered {
                    theme.colors.accent_hovered
                } else {
                    theme.colors.accent
                }
            });
        let mut background = colors
            .filter(|colors| colors.has_background())
            .map(|colors| colors.background_color())
            .unwrap_or(theme.colors.surface);
        let mut border_color = colors
            .filter(|colors| colors.has_border())
            .map(|colors| colors.border_color())
            .unwrap_or(theme.colors.border);
        let mut mark_visible = false;
        let mut mark_color = theme.colors.text_primary;
        if state.checked_state == SemanticCheckedState::True
            || state.checked_state == SemanticCheckedState::Mixed
        {
            background = accent;
            border_color = background;
            mark_visible = state.checked_state == SemanticCheckedState::True;
            mark_color = theme.colors.text_on_accent;
        } else if state.hovered && colors.is_none_or(|colors| !colors.has_background()) {
            background = theme.colors.background;
        }
        self.root
            .corner_radius(metrics.corner_radius)
            .border(1.0, border_color)
            .bg_color(background);
        if mark_visible {
            self.mark_node.source(CHECKBOX_CHECK_SVG_URL);
        } else {
            self.mark_node.clear_source();
        }
        self.mark_node
            .width(metrics.check_mark_size, Unit::Pixel)
            .height(metrics.check_mark_size, Unit::Pixel)
            .opacity(if mark_visible { 1.0 } else { 0.0 })
            .tint(mark_color);
    }
}

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

impl CheckboxIndicatorTemplate for DefaultCheckboxIndicatorTemplate {
    fn create(&self, sizing: Option<LabeledControlSizing>) -> Rc<dyn CheckboxIndicatorPresenter> {
        create_default_checkbox_indicator_presenter(sizing)
    }
}

pub const DEFAULT_CHECKBOX_INDICATOR_TEMPLATE: DefaultCheckboxIndicatorTemplate =
    DefaultCheckboxIndicatorTemplate;

pub fn create_default_checkbox_indicator_presenter(
    sizing: Option<LabeledControlSizing>,
) -> Rc<dyn CheckboxIndicatorPresenter> {
    Rc::new(DefaultCheckboxIndicatorPresenter::new(
        resolve_checkbox_metrics(sizing),
    ))
}