repose-core 0.31.24

Repose's core runtime, view model, signals, composition locals, and animation clock.
Documentation
use std::rc::Rc;

use crate::{
    Brush, Color, ImageAlignment, ImageFilter, ImageFit, ImageHandle, ImagePaintStyle,
    ImageSourceRect, Px, Rect, Scene, SceneNode,
};

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ControlVisualState {
    pub alpha: f32,
    pub enabled: bool,
    pub hovered: bool,
    pub pressed: bool,
    pub dragged: bool,
    pub focused: bool,
}

impl Default for ControlVisualState {
    fn default() -> Self {
        Self {
            alpha: 1.0,
            enabled: true,
            hovered: false,
            pressed: false,
            dragged: false,
            focused: false,
        }
    }
}

pub type ControlPainter = Rc<dyn Fn(&mut Scene, Rect, ControlVisualState)>;

#[derive(Clone)]
#[non_exhaustive]
pub enum ControlVisual {
    Rect {
        brush: Brush,
        radius: [Px; 4],
    },
    Image {
        handle: ImageHandle,
        source_rect: Option<ImageSourceRect>,
        tint: Color,
        fit: ImageFit,
        filter: ImageFilter,
        alignment: ImageAlignment,
    },
    Custom(ControlPainter),
}

impl ControlVisual {
    pub fn rect(brush: Brush, radius: [Px; 4]) -> Self {
        Self::Rect { brush, radius }
    }

    pub fn image(handle: ImageHandle, source_rect: Option<ImageSourceRect>, tint: Color) -> Self {
        Self::Image {
            handle,
            source_rect,
            tint,
            fit: ImageFit::FillBounds,
            filter: ImageFilter::Linear,
            alignment: ImageAlignment::Center,
        }
    }

    pub fn custom(painter: impl Fn(&mut Scene, Rect, ControlVisualState) + 'static) -> Self {
        Self::Custom(Rc::new(painter))
    }

    pub fn image_fit(mut self, fit: ImageFit) -> Self {
        if let Self::Image { fit: current, .. } = &mut self {
            *current = fit;
        }
        self
    }

    pub fn image_filter(mut self, filter: ImageFilter) -> Self {
        if let Self::Image {
            filter: current, ..
        } = &mut self
        {
            *current = filter;
        }
        self
    }

    pub fn image_alignment(mut self, alignment: ImageAlignment) -> Self {
        if let Self::Image {
            alignment: current, ..
        } = &mut self
        {
            *current = alignment;
        }
        self
    }

    pub fn paint(&self, scene: &mut Scene, rect: Rect, state: ControlVisualState) {
        match self {
            Self::Rect { brush, radius } => scene.nodes.push(SceneNode::Rect {
                rect,
                brush: scale_brush_alpha(*brush, state.alpha),
                radius: *radius,
            }),
            Self::Image {
                handle,
                source_rect,
                tint,
                fit,
                filter,
                alignment,
            } => scene.nodes.push(SceneNode::Image {
                rect,
                handle: *handle,
                tint: scale_color_alpha(*tint, state.alpha),
                style: ImagePaintStyle {
                    fit: *fit,
                    filter: *filter,
                    source_rect: *source_rect,
                    alignment: *alignment,
                },
            }),
            Self::Custom(painter) => painter(scene, rect, state),
        }
    }

    pub fn custom_identity(&self) -> Option<usize> {
        match self {
            Self::Custom(painter) => Some(Rc::as_ptr(painter) as *const () as usize),
            _ => None,
        }
    }
}

impl std::fmt::Debug for ControlVisual {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Rect { brush, radius } => f
                .debug_struct("Rect")
                .field("brush", brush)
                .field("radius", radius)
                .finish(),
            Self::Image {
                handle,
                source_rect,
                tint,
                fit,
                filter,
                alignment,
            } => f
                .debug_struct("Image")
                .field("handle", handle)
                .field("source_rect", source_rect)
                .field("tint", tint)
                .field("fit", fit)
                .field("filter", filter)
                .field("alignment", alignment)
                .finish(),
            Self::Custom(_) => f
                .debug_struct("Custom")
                .field("identity", &self.custom_identity())
                .finish(),
        }
    }
}

impl PartialEq for ControlVisual {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (
                Self::Rect {
                    brush: a,
                    radius: ra,
                },
                Self::Rect {
                    brush: b,
                    radius: rb,
                },
            ) => a == b && ra == rb,
            (
                Self::Image {
                    handle: ah,
                    source_rect: ar,
                    tint: at,
                    fit: af,
                    filter: ax,
                    alignment: aa,
                },
                Self::Image {
                    handle: bh,
                    source_rect: br,
                    tint: bt,
                    fit: bf,
                    filter: bx,
                    alignment: ba,
                },
            ) => ah == bh && ar == br && at == bt && af == bf && ax == bx && aa == ba,
            (Self::Custom(a), Self::Custom(b)) => Rc::ptr_eq(a, b),
            _ => false,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct ControlVisualSet {
    pub normal: Option<ControlVisual>,
    pub hovered: Option<ControlVisual>,
    pub pressed: Option<ControlVisual>,
    pub dragged: Option<ControlVisual>,
    pub focused: Option<ControlVisual>,
    pub disabled: Option<ControlVisual>,
}

impl ControlVisualSet {
    pub fn resolve(&self, state: ControlVisualState) -> Option<&ControlVisual> {
        let selected = if !state.enabled {
            self.disabled.as_ref()
        } else if state.dragged {
            self.dragged.as_ref()
        } else if state.pressed {
            self.pressed.as_ref()
        } else if state.focused {
            self.focused.as_ref()
        } else if state.hovered {
            self.hovered.as_ref()
        } else {
            None
        };
        selected.or(self.normal.as_ref())
    }

    pub fn with_normal(mut self, visual: ControlVisual) -> Self {
        self.normal = Some(visual);
        self
    }

    pub fn with_hovered(mut self, visual: ControlVisual) -> Self {
        self.hovered = Some(visual);
        self
    }

    pub fn with_pressed(mut self, visual: ControlVisual) -> Self {
        self.pressed = Some(visual);
        self
    }

    pub fn with_dragged(mut self, visual: ControlVisual) -> Self {
        self.dragged = Some(visual);
        self
    }

    pub fn with_focused(mut self, visual: ControlVisual) -> Self {
        self.focused = Some(visual);
        self
    }

    pub fn with_disabled(mut self, visual: ControlVisual) -> Self {
        self.disabled = Some(visual);
        self
    }
}

fn scale_color_alpha(color: Color, alpha: f32) -> Color {
    Color(
        color.0,
        color.1,
        color.2,
        (color.3 as f32 * alpha.clamp(0.0, 1.0)) as u8,
    )
}

fn scale_brush_alpha(brush: Brush, alpha: f32) -> Brush {
    match brush {
        Brush::Solid(color) => Brush::Solid(scale_color_alpha(color, alpha)),
        Brush::Linear {
            start,
            end,
            start_color,
            end_color,
        } => Brush::Linear {
            start,
            end,
            start_color: scale_color_alpha(start_color, alpha),
            end_color: scale_color_alpha(end_color, alpha),
        },
        Brush::LinearNormalized {
            start,
            end,
            start_color,
            end_color,
        } => Brush::LinearNormalized {
            start,
            end,
            start_color: scale_color_alpha(start_color, alpha),
            end_color: scale_color_alpha(end_color, alpha),
        },
        Brush::Radial {
            center,
            radius,
            start_color,
            end_color,
        } => Brush::Radial {
            center,
            radius,
            start_color: scale_color_alpha(start_color, alpha),
            end_color: scale_color_alpha(end_color, alpha),
        },
        Brush::Sweep {
            center,
            start_color,
            end_color,
        } => Brush::Sweep {
            center,
            start_color: scale_color_alpha(start_color, alpha),
            end_color: scale_color_alpha(end_color, alpha),
        },
    }
}

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

    #[test]
    fn control_visual_state_priority_is_stable() {
        let set = ControlVisualSet {
            normal: Some(ControlVisual::image(1, None, Color::WHITE)),
            hovered: Some(ControlVisual::image(2, None, Color::WHITE)),
            focused: Some(ControlVisual::image(3, None, Color::WHITE)),
            dragged: Some(ControlVisual::image(4, None, Color::WHITE)),
            disabled: Some(ControlVisual::image(5, None, Color::WHITE)),
            ..Default::default()
        };
        let focused_hovered = ControlVisualState {
            hovered: true,
            focused: true,
            ..Default::default()
        };
        assert!(matches!(
            set.resolve(focused_hovered),
            Some(ControlVisual::Image { handle: 3, .. })
        ));
        let disabled_dragged = ControlVisualState {
            enabled: false,
            dragged: true,
            ..Default::default()
        };
        assert!(matches!(
            set.resolve(disabled_dragged),
            Some(ControlVisual::Image { handle: 5, .. })
        ));
    }
}