use dioxus::prelude::*;
use dioxus_primitives::color_picker::{
self, Color, ColorAreaProps, ColorPickerContext,
};
use dioxus_primitives::popover;
use dioxus_primitives::use_controlled;
use dioxus_primitives::label::Label;
use dioxus_primitives::slider::*;
use palette::{encoding, FromColor, Hsv, IntoColor, RgbHue, Srgb};
use crate::components::input::Input;
#[css_module("/src/components/color_picker/style.css")]
struct Styles;
fn format_color_hex(color: Color) -> String {
format!("#{color:X}")
}
#[derive(Clone, Copy)]
struct ColorPickerRootContext {
open: Memo<bool>,
disabled: ReadSignal<bool>,
color: ReadSignal<Hsv<encoding::Srgb, f64>>,
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorPickerRootProps {
#[props(default)]
pub color: ReadSignal<Hsv<encoding::Srgb, f64>>,
#[props(default)]
pub on_color_change: Callback<Hsv<encoding::Srgb, f64>>,
#[props(default)]
pub disabled: ReadSignal<bool>,
pub open: ReadSignal<Option<bool>>,
#[props(default)]
pub default_open: bool,
#[props(default)]
pub on_open_change: Callback<bool>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn ColorPickerRoot(props: ColorPickerRootProps) -> Element {
let (open, set_open) = use_controlled(props.open, props.default_open, props.on_open_change);
use_context_provider(|| ColorPickerRootContext {
open,
disabled: props.disabled,
color: props.color,
});
rsx! {
color_picker::ColorPicker {
class: Styles::dx_color_picker,
color: props.color,
on_color_change: props.on_color_change,
disabled: props.disabled,
attributes: props.attributes,
popover::PopoverRoot {
is_modal: false,
open: Some(open()),
on_open_change: move |v| set_open.call(v),
{props.children}
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorPickerProps {
#[props(default)]
pub color: ReadSignal<Hsv<encoding::Srgb, f64>>,
#[props(default)]
pub on_color_change: Callback<Hsv<encoding::Srgb, f64>>,
#[props(default)]
pub disabled: ReadSignal<bool>,
#[props(default)]
pub label: Option<String>,
pub open: ReadSignal<Option<bool>>,
#[props(default)]
pub default_open: bool,
#[props(default)]
pub on_open_change: Callback<bool>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn ColorPicker(props: ColorPickerProps) -> Element {
rsx! {
ColorPickerRoot {
color: props.color,
on_color_change: props.on_color_change,
disabled: props.disabled,
open: props.open,
default_open: props.default_open,
on_open_change: props.on_open_change,
attributes: props.attributes,
ColorPickerTrigger {
label: props.label,
}
ColorPickerPopover {
ColorPickerSelect {}
{props.children}
}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorPickerTriggerProps {
#[props(default)]
pub label: Option<String>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn ColorPickerTrigger(props: ColorPickerTriggerProps) -> Element {
let ctx = use_context::<ColorPickerRootContext>();
let aria_hex = use_memo(move || {
let rgb: Color = Srgb::<f64>::from_color((ctx.color)()).into_format();
format_color_hex(rgb)
});
rsx! {
popover::PopoverTrigger {
class: Styles::dx_color_picker_button,
disabled: if (ctx.disabled)() { true },
aria_label: format!("Color picker {aria_hex}"),
aria_expanded: (ctx.open)(),
attributes: props.attributes,
ColorSwatch { color: ctx.color }
if let Some(label) = props.label { span { {label} } }
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorPickerPopoverProps {
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn ColorPickerPopover(props: ColorPickerPopoverProps) -> Element {
rsx! {
popover::PopoverContent {
class: Styles::dx_color_picker_popover.to_string(),
attributes: props.attributes,
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorFieldProps {
#[props(default)]
pub label: Option<String>,
#[props(default)]
pub description: Option<String>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
fn ColorField(props: ColorFieldProps) -> Element {
let ctx = use_context::<ColorPickerContext>();
let hex_from_hsv = |hsv: Hsv<encoding::Srgb, f64>| {
let rgb: Color = Srgb::<f64>::from_color(hsv).into_format();
format_color_hex(rgb)
};
let emit_rgb = move |rgb: Color| {
let hsv: Hsv<encoding::Srgb, f64> = rgb.into_format::<f64>().into_color();
ctx.set_color(hsv);
};
let mut value = use_signal(|| hex_from_hsv(ctx.color()));
use_effect(move || {
let external = ctx.color();
let current = value();
if let Ok(parsed) = current.parse::<Color>() {
let external_rgb: Color = Srgb::<f64>::from_color(external).into_format();
if parsed != external_rgb {
value.set(hex_from_hsv(external));
}
} else if current.is_empty() {
value.set(hex_from_hsv(external));
}
});
rsx! {
div {
class: Styles::dx_color_field_container,
..props.attributes,
if let Some(label) = props.label {
Label {
html_for: "color_field",
class: Styles::dx_color_slider_title,
{label}
}
}
Input {
id: "color_field",
placeholder: "Enter a color",
value: "{value}",
oninput: move |e: FormEvent| {
let mut input = e.value();
input.retain(|c| c == '#' || c.is_ascii_hexdigit());
if !input.starts_with('#') && !input.is_empty() {
input.insert(0, '#');
}
input.truncate(7);
value.set(input.to_uppercase());
if let Ok(parsed) = input.parse::<Color>() {
emit_rgb(parsed);
}
},
}
if let Some(text) = props.description {
span { class: Styles::dx_color_field_description, {text} }
}
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorSwatchProps {
#[props(default)]
pub color: ReadSignal<Hsv<encoding::Srgb, f64>>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
fn ColorSwatch(props: ColorSwatchProps) -> Element {
let hex_color = use_memo(move || {
let rgb: Color = Srgb::<f64>::from_color((props.color)()).into_format();
format_color_hex(rgb)
});
rsx! {
div {
role: "img",
aria_label: format!("Selected color {hex_color}"),
class: Styles::dx_color_swatch,
style: "--swatch-color: {hex_color}",
..props.attributes,
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorSliderProps {
pub title: ReadSignal<String>,
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
fn ColorSlider(props: ColorSliderProps) -> Element {
let ctx = use_context::<ColorPickerContext>();
let mut current_hue = use_signal(|| ctx.color().hue.into_positive_degrees());
let thumb_color = use_memo(move || {
Srgb::<f64>::from_color(Hsv::<encoding::Srgb, f64>::new(
RgbHue::new(current_hue()),
1.0,
1.0,
))
.into_format()
});
use_effect(move || {
let value = ctx.color().hue.into_positive_degrees();
let current = current_hue();
let is_wrap_around = (value - current).abs() > 350.0;
if !is_wrap_around && value != current {
current_hue.set(value);
}
});
let display_value = {
let value = current_hue();
format!("{value:.2}")
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
+ "°"
};
rsx! {
div {
class: Styles::dx_color_slider_container,
..props.attributes,
label { class: Styles::dx_color_slider_title, {props.title} }
output { class: Styles::dx_color_slider_output, "{display_value}" }
Slider {
class: Styles::dx_color_slider,
label: "Color Slider",
horizontal: true,
max: 360.0,
value: current_hue(),
on_value_change: move |h: f64| {
current_hue.set(h);
ctx.set_hue(h);
},
SliderTrack {
class: Styles::dx_color_slider_track,
SliderThumb {
class: Styles::dx_color_slider_thumb,
aria_label: "Hue",
aria_valuetext: format!("{:.0}°", current_hue()),
background_color: format_color_hex(thumb_color()),
}
}
}
{props.children}
}
}
}
#[component]
fn ColorArea(props: ColorAreaProps) -> Element {
rsx! {
color_picker::ColorArea {
class: Styles::dx_color_area_container,
step: props.step,
attributes: props.attributes,
color_picker::AreaTrack {
class: Styles::dx_color_area_track,
color_picker::AreaThumb {
class: Styles::dx_color_area_thumb,
color_picker::AreaThumbSaturationInput {
class: Styles::dx_color_area_input,
}
color_picker::AreaThumbValueInput {
class: Styles::dx_color_area_input,
}
}
}
{props.children}
}
}
}
#[derive(Props, Clone, PartialEq)]
pub struct ColorPickerSelectProps {
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
pub children: Element,
}
#[component]
pub fn ColorPickerSelect(props: ColorPickerSelectProps) -> Element {
let ctx = use_context::<ColorPickerContext>();
rsx! {
div {
class: Styles::dx_color_picker_dialog,
..props.attributes,
ColorArea {}
ColorSlider { title: "Hue" }
div {
class: Styles::dx_color_picker_input,
ColorField { label: "Hex" }
ColorSwatch { color: ctx.color() }
}
}
}
}