freya-components 0.4.0-rc.18

Components for Freya apps
Documentation
use freya_animation::{
    easing::Function,
    hook::{
        AnimatedValue,
        Ease,
        OnChange,
        OnCreation,
        ReadAnimatedValue,
        use_animation,
    },
    prelude::AnimNum,
};
use freya_core::prelude::*;
use freya_edit::Clipboard;
use torin::prelude::{
    Alignment,
    Area,
    CursorPoint,
    Position,
    Size,
};

use crate::{
    button::Button,
    context_menu::ContextMenu,
    define_theme,
    get_theme,
    menu::{
        Menu,
        MenuButton,
    },
};

define_theme! {
    %[component]
    pub ColorPicker {
        %[fields]
        background: Color,
        color: Color,
        border_fill: Color,
    }
}

/// HSV-based gradient color picker.
///
/// ## Example
///
/// ```rust
/// # use freya::prelude::*;
/// fn app() -> impl IntoElement {
///     let mut color = use_state(|| Color::from_hsv(0.0, 1.0, 1.0));
///     rect()
///         .padding(6.)
///         .child(ColorPicker::new(move |c| color.set(c)).value(color()))
/// }
/// # use freya_testing::prelude::*;
/// # use std::time::Duration;
/// # launch_doc(|| {
/// #     rect().padding(6.).child(app())
/// # }, "./images/gallery_color_picker.png").with_hook(|t| { t.move_cursor((15., 15.)); t.click_cursor((15., 15.)); t.poll(Duration::from_millis(1), Duration::from_millis(250)); }).with_scale_factor(0.85).render();
/// ```
///
/// # Preview
/// ![ColorPicker Preview][gallery_color_picker]
#[cfg_attr(feature = "docs",
    doc = embed_doc_image::embed_image!("gallery_color_picker", "images/gallery_color_picker.png"),
)]
///
/// The preview image is generated by simulating a click on the preview so the popup is shown.
/// This is done using the `with_hook` helper in the doc test to move the cursor and click the preview.
#[derive(Clone, PartialEq)]
pub struct ColorPicker {
    pub(crate) theme: Option<ColorPickerThemePartial>,
    value: Color,
    on_change: EventHandler<Color>,
    width: Size,
    key: DiffKey,
}

impl KeyExt for ColorPicker {
    fn write_key(&mut self) -> &mut DiffKey {
        &mut self.key
    }
}

impl ColorPicker {
    pub fn new(on_change: impl Into<EventHandler<Color>>) -> Self {
        Self {
            theme: None,
            value: Color::WHITE,
            on_change: on_change.into(),
            width: Size::px(220.),
            key: DiffKey::None,
        }
    }

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

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

/// Which part of the color picker is being dragged, if any.
#[derive(Clone, Copy, PartialEq, Default)]
enum DragTarget {
    #[default]
    None,
    Sv,
    Hue,
}

impl Component for ColorPicker {
    fn render(&self) -> impl IntoElement {
        let mut open = use_state(|| false);
        let mut color = use_state(|| self.value);
        let mut dragging = use_state(DragTarget::default);
        let mut area = use_state(Area::default);
        let mut hue_area = use_state(Area::default);

        let is_open = open();

        let preview = rect()
            .width(Size::px(40.))
            .height(Size::px(24.))
            .corner_radius(4.)
            .background(self.value)
            .on_press(move |_| {
                open.toggle();
            });

        let theme = get_theme!(&self.theme, ColorPickerThemePreference, "color_picker");
        let hue_bar = rect()
            .height(Size::px(18.))
            .width(Size::fill())
            .corner_radius(4.)
            .on_sized(move |e: Event<SizedEventData>| hue_area.set(e.area))
            .background_linear_gradient(
                LinearGradient::new()
                    .angle(-90.)
                    .stop(((255, 0, 0), 0.))
                    .stop(((255, 255, 0), 16.))
                    .stop(((0, 255, 0), 33.))
                    .stop(((0, 255, 255), 50.))
                    .stop(((0, 0, 255), 66.))
                    .stop(((255, 0, 255), 83.))
                    .stop(((255, 0, 0), 100.)),
            );

        let sv_area = rect()
            .height(Size::px(140.))
            .width(Size::fill())
            .corner_radius(4.)
            .overflow(Overflow::Clip)
            .child(
                rect()
                    .expanded()
                    .background_linear_gradient(
                        // left: white -> right: hue color
                        LinearGradient::new()
                            .angle(-90.)
                            .stop(((255, 255, 255), 0.))
                            .stop((Color::from_hsv(color.read().to_hsv().h, 1.0, 1.0), 100.)),
                    )
                    .child(
                        rect()
                            .position(Position::new_absolute())
                            .expanded()
                            .background_linear_gradient(
                                // top: transparent -> bottom: black
                                LinearGradient::new()
                                    .stop(((255, 255, 255, 0.0), 0.))
                                    .stop(((0, 0, 0), 100.)),
                            ),
                    ),
            );

        // Minimum perceptible floor to avoid full desaturation/black when dragging
        const MIN_S: f32 = 0.07;
        const MIN_V: f32 = 0.07;

        let mut update_sv = {
            let on_change = self.on_change.clone();
            move |coords: CursorPoint| {
                let sv_area = area.read().to_f64();
                let rel_x = (((coords.x - sv_area.min_x()) / sv_area.width()).clamp(0., 1.)) as f32;
                let rel_y = (((coords.y - sv_area.min_y()) / sv_area.height())
                    .clamp(MIN_V as f64, 1. - MIN_V as f64)) as f32;
                let sat = rel_x.max(MIN_S);
                let v = (1.0 - rel_y).clamp(MIN_V, 1.0 - MIN_V);
                let hsv = color.read().to_hsv();
                color.set(Color::from_hsv(hsv.h, sat, v));
                on_change.call(color());
            }
        };

        let mut update_hue = {
            let on_change = self.on_change.clone();
            move |coords: CursorPoint| {
                let bar_area = hue_area.read().to_f64();
                let rel_x =
                    ((coords.x - bar_area.min_x()) / bar_area.width()).clamp(0.01, 1.) as f32;
                let hsv = color.read().to_hsv();
                color.set(Color::from_hsv(rel_x * 360.0, hsv.s, hsv.v));
                on_change.call(color());
            }
        };

        let on_sv_pointer_down = {
            let mut update_sv = update_sv.clone();
            move |e: Event<PointerEventData>| {
                dragging.set(DragTarget::Sv);
                update_sv(e.global_location());
                e.stop_propagation();
                e.prevent_default();
            }
        };

        let on_hue_pointer_down = {
            let mut update_hue = update_hue.clone();
            move |e: Event<PointerEventData>| {
                dragging.set(DragTarget::Hue);
                update_hue(e.global_location());
                e.stop_propagation();
                e.prevent_default();
            }
        };

        let on_global_pointer_move = move |e: Event<PointerEventData>| match *dragging.read() {
            DragTarget::Sv => {
                update_sv(e.global_location());
            }
            DragTarget::Hue => {
                update_hue(e.global_location());
            }
            DragTarget::None => {}
        };

        let on_global_pointer_press = move |_| {
            // Only close the popup if it wasnt being dragged and it is open
            if is_open && dragging() == DragTarget::None {
                open.set(false);
            }
            dragging.set_if_modified(DragTarget::None);
        };

        let animation = use_animation(move |conf| {
            conf.on_change(OnChange::Rerun);
            conf.on_creation(OnCreation::Finish);

            let scale = AnimNum::new(0.8, 1.)
                .time(200)
                .ease(Ease::Out)
                .function(Function::Expo);
            let opacity = AnimNum::new(0., 1.)
                .time(200)
                .ease(Ease::Out)
                .function(Function::Expo);

            if open() {
                (scale, opacity)
            } else {
                (scale, opacity).into_reversed()
            }
        });

        let (scale, opacity) = animation.read().value();

        let popup = rect()
            .on_global_pointer_move(on_global_pointer_move)
            .on_global_pointer_press(on_global_pointer_press)
            .width(self.width.clone())
            .padding(8.)
            .corner_radius(6.)
            .background(theme.background)
            .border(
                Border::new()
                    .fill(theme.border_fill)
                    .width(1.)
                    .alignment(BorderAlignment::Inner),
            )
            .color(theme.color)
            .spacing(8.)
            .shadow(Shadow::new().x(0.).y(2.).blur(8.).color((0, 0, 0, 0.1)))
            .child(
                rect()
                    .on_sized(move |e: Event<SizedEventData>| area.set(e.area))
                    .on_pointer_down(on_sv_pointer_down)
                    .child(sv_area),
            )
            .child(
                rect()
                    .height(Size::px(18.))
                    .on_pointer_down(on_hue_pointer_down)
                    .child(hue_bar),
            )
            .child({
                let hex = format!(
                    "#{:02X}{:02X}{:02X}",
                    color.read().r(),
                    color.read().g(),
                    color.read().b()
                );

                rect()
                    .horizontal()
                    .width(Size::fill())
                    .main_align(Alignment::center())
                    .spacing(8.)
                    .child(
                        Button::new()
                            .on_press(move |e: Event<PressEventData>| {
                                e.stop_propagation();
                                e.prevent_default();
                                if ContextMenu::is_open() {
                                    ContextMenu::close();
                                } else {
                                    ContextMenu::open_from_event(
                                        &e,
                                        Menu::new()
                                            .child(
                                                MenuButton::new()
                                                    .on_press(move |e: Event<PressEventData>| {
                                                        e.stop_propagation();
                                                        e.prevent_default();
                                                        ContextMenu::close();
                                                        let _ =
                                                            Clipboard::set(color().to_rgb_string());
                                                    })
                                                    .child("Copy as RGB"),
                                            )
                                            .child(
                                                MenuButton::new()
                                                    .on_press(move |e: Event<PressEventData>| {
                                                        e.stop_propagation();
                                                        e.prevent_default();
                                                        ContextMenu::close();
                                                        let _ =
                                                            Clipboard::set(color().to_hex_string());
                                                    })
                                                    .child("Copy as HEX"),
                                            ),
                                    )
                                }
                            })
                            .compact()
                            .child(hex),
                    )
            });

        rect()
            .horizontal()
            .spacing(8.)
            .child(preview)
            .maybe_child((opacity > 0.).then(|| {
                rect()
                    .layer(Layer::Overlay)
                    .width(Size::px(0.))
                    .height(Size::px(0.))
                    .opacity(opacity)
                    .child(rect().scale(scale).child(popup))
            }))
    }

    fn render_key(&self) -> DiffKey {
        self.key.clone().or(self.default_key())
    }
}