use crate::{
input_ui::{Input, InputState},
prelude::*,
*,
};
use std::sync::Arc;
pub struct ColorPickerState {
hue: Entity<SliderState>,
saturation: Entity<SliderState>,
lightness: Entity<SliderState>,
hex_input: Entity<InputState>,
color: Hsla,
on_change: Option<Arc<dyn Fn(Hsla, &mut Window, &mut App) + Send + Sync + 'static>>,
pending_emit: bool,
hex_dirty: bool,
}
impl ColorPickerState {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let hue = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(360.0)
.step(1.0)
.default_value(210.0)
});
let saturation = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.step(1.0)
.default_value(60.0)
});
let lightness = cx.new(|_| {
SliderState::new()
.min(0.0)
.max(100.0)
.step(1.0)
.default_value(50.0)
});
let hex_input = cx.new(|cx| InputState::new(window, cx).placeholder("#rrggbb"));
for slider in [&hue, &saturation, &lightness] {
cx.subscribe(slider, |this, _slider, event, cx| match event {
SliderEvent::Change(_) => {
this.recompute_from_sliders(cx);
}
_ => {}
})
.detach();
}
cx.subscribe(&hex_input, |this, _input, event, cx| match event {
crate::input_ui::InputEvent::Change => {
this.recompute_from_hex(cx);
}
_ => {}
})
.detach();
Self {
hue,
saturation,
lightness,
hex_input,
color: hsla(210.0 / 360.0, 0.6, 0.5, 1.0),
on_change: None,
pending_emit: false,
hex_dirty: true,
}
}
pub fn color(mut self, color: Hsla) -> Self {
self.color = color;
self
}
pub fn on_change<F>(mut self, f: F) -> Self
where
F: Fn(Hsla, &mut Window, &mut App) + Send + Sync + 'static,
{
self.on_change = Some(Arc::new(f));
self
}
pub fn color_value(&self) -> Hsla {
self.color
}
fn recompute_from_sliders(&mut self, cx: &mut Context<Self>) {
let h = slider_value(&self.hue, cx);
let s = slider_value(&self.saturation, cx);
let l = slider_value(&self.lightness, cx);
self.color = hsla(h / 360.0, s / 100.0, l / 100.0, 1.0);
self.hex_dirty = true;
self.pending_emit = true;
cx.notify();
}
fn recompute_from_hex(&mut self, cx: &mut Context<Self>) {
let text = self.hex_input.read(cx).text().to_string();
let hex = text.trim().trim_start_matches('#');
if hex.len() != 6 {
return;
}
let rgb = u32::from_str_radix(hex, 16).ok();
if let Some(rgb) = rgb {
let r = ((rgb >> 16) & 0xff) as f32 / 255.0;
let g = ((rgb >> 8) & 0xff) as f32 / 255.0;
let b = (rgb & 0xff) as f32 / 255.0;
self.color = rgb_to_hsla(r, g, b);
self.pending_emit = true;
cx.notify();
}
}
}
fn slider_value(state: &Entity<SliderState>, cx: &App) -> f32 {
match state.read(cx).value() {
SliderValue::Single(v) => v,
SliderValue::Range(_, end) => end,
}
}
fn rgb_to_hsla(r: f32, g: f32, b: f32) -> Hsla {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let l = (max + min) / 2.0;
if (max - min).abs() < f32::EPSILON {
return hsla(0.0, 0.0, l, 1.0);
}
let d = max - min;
let s = if l > 0.5 {
d / (2.0 - max - min)
} else {
d / (max + min)
};
let h = if (max - r).abs() < f32::EPSILON {
(g - b) / d + if g < b { 6.0 } else { 0.0 }
} else if (max - g).abs() < f32::EPSILON {
(b - r) / d + 2.0
} else {
(r - g) / d + 4.0
} / 6.0;
hsla(h, s, l, 1.0)
}
fn hsla_to_rgb(color: Hsla) -> (f32, f32, f32) {
let (h, s, l) = (color.h, color.s, color.l);
if s.abs() < f32::EPSILON {
return (l, l, l);
}
let q = if l < 0.5 {
l * (1.0 + s)
} else {
l + s - l * s
};
let p = 2.0 * l - q;
let channel = |t: f32| {
let t = if t < 0.0 {
t + 1.0
} else if t > 1.0 {
t - 1.0
} else {
t
};
if t < 1.0 / 6.0 {
p + (q - p) * 6.0 * t
} else if t < 0.5 {
q
} else if t < 2.0 / 3.0 {
p + (q - p) * (2.0 / 3.0 - t) * 6.0
} else {
p
}
};
(channel(h + 1.0 / 3.0), channel(h), channel(h - 1.0 / 3.0))
}
impl Render for ColorPickerState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.hex_dirty {
self.hex_dirty = false;
let (r, g, b) = hsla_to_rgb(self.color);
let text: SharedString = format!(
"#{:02x}{:02x}{:02x}",
(r * 255.0).round() as u8,
(g * 255.0).round() as u8,
(b * 255.0).round() as u8
)
.into();
self.hex_input.update(cx, |input, cx| {
input.set_value(text, window, cx);
});
}
if self.pending_emit {
self.pending_emit = false;
if let Some(ref cb) = self.on_change.clone() {
cb(self.color, window, cx);
}
}
let theme = cx.theme();
let border = theme.tokens.border;
let popover = theme.tokens.popover;
let muted_foreground = theme.tokens.muted_foreground.color;
let hue = self.hue.clone();
let saturation = self.saturation.clone();
let lightness = self.lightness.clone();
let hex_input = self.hex_input.clone();
div()
.flex()
.flex_col()
.w(px(260.0))
.bg(popover)
.border_1()
.border_color(border)
.rounded_md()
.p(px(12.0))
.gap(px(8.0))
.child(div().w_full().h(px(48.0)).rounded_sm().bg(self.color))
.child(slider_row("H", &hue, muted_foreground))
.child(slider_row("S", &saturation, muted_foreground))
.child(slider_row("L", &lightness, muted_foreground))
.child(Input::new(&hex_input).w_full())
}
}
fn slider_row(
label: &'static str,
state: &Entity<SliderState>,
muted_foreground: Hsla,
) -> impl IntoElement {
div()
.flex()
.flex_row()
.items_center()
.gap(px(8.0))
.child(
div()
.w(px(16.0))
.text_sm()
.text_color(muted_foreground)
.child(label),
)
.child(div().flex_1().child(Slider::new(state).horizontal()))
}