1use std::cell::Cell;
17use std::rc::Rc;
18
19use gpui::{
20 Axis, Bounds, Context, DragMoveEvent, Entity, EntityId, EventEmitter, Hsla, MouseButton,
21 MouseDownEvent, Pixels, Point, Rgba, Subscription, Window, canvas, div, linear_color_stop,
22 linear_gradient, prelude::*, px,
23};
24use theme::{TextStyle, Theme, Typeset};
25
26use crate::input::{FieldEvent, TextField};
27use crate::{stack, widgets};
28
29const FIELD_HEIGHT: f32 = 96.0;
30const STRIP_HEIGHT: f32 = 12.0;
31const KNOB: f32 = 12.0;
32
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub enum ColorPickerEvent {
35 Changed(Hsla),
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
41struct Hsva {
42 h: f32,
43 s: f32,
44 v: f32,
45 a: f32,
46}
47
48impl Hsva {
49 fn from_hsla(color: Hsla) -> Self {
50 let Rgba { r, g, b, a } = color.to_rgb();
51 let max = r.max(g).max(b);
52 let delta = max - r.min(g).min(b);
53 let h = if delta <= f32::EPSILON {
54 color.h
55 } else if max == r {
56 ((g - b) / delta).rem_euclid(6.0) / 6.0
57 } else if max == g {
58 ((b - r) / delta + 2.0) / 6.0
59 } else {
60 ((r - g) / delta + 4.0) / 6.0
61 };
62 let s = if max <= f32::EPSILON {
63 0.0
64 } else {
65 delta / max
66 };
67 Self { h, s, v: max, a }
68 }
69
70 fn to_hsla(self) -> Hsla {
71 let Self { h, s, v, a } = self;
72 let sector = (h.rem_euclid(1.0)) * 6.0;
73 let chroma = v * s;
74 let x = chroma * (1.0 - (sector.rem_euclid(2.0) - 1.0).abs());
75 let (r, g, b) = match sector as u32 {
76 0 => (chroma, x, 0.0),
77 1 => (x, chroma, 0.0),
78 2 => (0.0, chroma, x),
79 3 => (0.0, x, chroma),
80 4 => (x, 0.0, chroma),
81 _ => (chroma, 0.0, x),
82 };
83 let m = v - chroma;
84 Rgba {
85 r: r + m,
86 g: g + m,
87 b: b + m,
88 a,
89 }
90 .into()
91 }
92}
93
94pub fn to_hex(color: Hsla, alpha: bool) -> String {
96 let Rgba { r, g, b, a } = color.to_rgb();
97 let byte = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
98 match alpha {
99 true => format!(
100 "#{:02x}{:02x}{:02x}{:02x}",
101 byte(r),
102 byte(g),
103 byte(b),
104 byte(a)
105 ),
106 false => format!("#{:02x}{:02x}{:02x}", byte(r), byte(g), byte(b)),
107 }
108}
109
110pub fn parse_hex(text: &str) -> Option<Hsla> {
112 let hex = text.trim();
113 let hex = hex.strip_prefix('#').unwrap_or(hex);
114 if !matches!(hex.len(), 6 | 8) || !hex.is_ascii() {
115 return None;
116 }
117 let byte = |at: usize| u8::from_str_radix(&hex[at..at + 2], 16).ok();
118 let alpha = match hex.len() {
119 8 => byte(6)?,
120 _ => 255,
121 };
122 let channel = |value: u8| value as f32 / 255.0;
123 Some(
124 Rgba {
125 r: channel(byte(0)?),
126 g: channel(byte(2)?),
127 b: channel(byte(4)?),
128 a: channel(alpha),
129 }
130 .into(),
131 )
132}
133
134#[derive(Clone, Copy, PartialEq, Eq)]
135enum Area {
136 Field,
137 Hue,
138 Alpha,
139}
140
141struct PickerDrag(EntityId, Area);
144
145pub struct ColorPicker {
146 hsva: Hsva,
147 alpha: bool,
148 hex: Entity<TextField>,
149 bounds: [Rc<Cell<Bounds<Pixels>>>; 3],
150 _hex: Subscription,
151}
152
153impl EventEmitter<ColorPickerEvent> for ColorPicker {}
154
155impl ColorPicker {
156 pub fn new(color: Hsla, alpha: bool, cx: &mut Context<Self>) -> Self {
159 let mut hsva = Hsva::from_hsla(color);
160 if !alpha {
161 hsva.a = 1.0;
162 }
163 let text = to_hex(hsva.to_hsla(), alpha);
164 let hex = cx.new(|cx| {
165 let mut field = TextField::new(cx).with_placeholder(match alpha {
166 true => "#rrggbbaa",
167 false => "#rrggbb",
168 });
169 field.set_content(text, cx);
170 field
171 });
172 let subscription = cx.subscribe(&hex, |this, field, event, cx| {
173 if !matches!(event, FieldEvent::Changed(_)) {
174 return;
175 }
176 let Some(color) = parse_hex(field.read(cx).content())
178 .filter(|color| to_hex(*color, this.alpha) != to_hex(this.color(), this.alpha))
179 else {
180 return;
181 };
182 let mut hsva = Hsva::from_hsla(color);
183 if !this.alpha {
184 hsva.a = 1.0;
185 }
186 hsva.h = match hsva.s <= f32::EPSILON {
187 true => this.hsva.h,
188 false => hsva.h,
189 };
190 this.hsva = hsva;
191 cx.emit(ColorPickerEvent::Changed(hsva.to_hsla()));
192 cx.notify();
193 });
194 Self {
195 hsva,
196 alpha,
197 hex,
198 bounds: Default::default(),
199 _hex: subscription,
200 }
201 }
202
203 pub fn color(&self) -> Hsla {
204 self.hsva.to_hsla()
205 }
206
207 pub fn set_color(&mut self, color: Hsla, cx: &mut Context<Self>) {
210 if to_hex(color, self.alpha) == to_hex(self.color(), self.alpha) {
211 return;
212 }
213 let mut hsva = Hsva::from_hsla(color);
214 if !self.alpha {
215 hsva.a = 1.0;
216 }
217 self.hsva = hsva;
218 self.sync_hex(cx);
219 cx.notify();
220 }
221
222 fn sync_hex(&mut self, cx: &mut Context<Self>) {
223 let text = to_hex(self.color(), self.alpha);
224 self.hex.update(cx, |field, cx| field.set_content(text, cx));
225 }
226
227 fn point(&mut self, area: Area, position: Point<Pixels>, cx: &mut Context<Self>) {
228 let mut bounds = self.bounds[area as usize].get();
229 if area != Area::Field {
230 bounds.origin.x += px(KNOB / 2.0);
233 bounds.size.width -= px(KNOB);
234 }
235 let x = widgets::axis_fraction(position, bounds, Axis::Horizontal, 0.0);
236 match area {
237 Area::Field => {
238 self.hsva.s = x;
239 self.hsva.v = 1.0 - widgets::axis_fraction(position, bounds, Axis::Vertical, 0.0);
240 }
241 Area::Hue => self.hsva.h = x,
242 Area::Alpha => self.hsva.a = x,
243 }
244 self.sync_hex(cx);
245 cx.emit(ColorPickerEvent::Changed(self.color()));
246 cx.notify();
247 }
248
249 fn area(
252 &self,
253 area: Area,
254 body: gpui::Div,
255 cx: &mut Context<Self>,
256 ) -> gpui::Stateful<gpui::Div> {
257 let entity = cx.entity_id();
258 let bounds = self.bounds[area as usize].clone();
259 body.id(("color-picker", area as usize))
260 .relative()
261 .cursor_pointer()
262 .child(
263 canvas(move |rect, _, _| bounds.set(rect), |_, _, _, _| {})
264 .absolute()
265 .size_full(),
266 )
267 .on_mouse_down(
268 MouseButton::Left,
269 cx.listener(move |this, event: &MouseDownEvent, _, cx| {
270 this.point(area, event.position, cx)
271 }),
272 )
273 .on_drag(PickerDrag(entity, area), |_, _, _, cx| {
274 cx.new(|_| gpui::Empty)
275 })
276 .on_drag_move(
277 cx.listener(move |this, event: &DragMoveEvent<PickerDrag>, _, cx| {
278 let PickerDrag(owner, dragged) = *event.drag(cx);
279 if owner == entity && dragged == area {
280 this.point(area, event.event.position, cx);
281 }
282 }),
283 )
284 }
285}
286
287fn knob_face() -> gpui::Div {
288 div()
289 .absolute()
290 .size(px(KNOB))
291 .rounded_full()
292 .border_2()
293 .border_color(gpui::white())
294 .shadow_sm()
295}
296
297fn field_knob(left: f32, top: f32) -> gpui::Div {
299 knob_face()
300 .left(gpui::relative(left))
301 .top(gpui::relative(top))
302 .ml(px(-KNOB / 2.0))
303 .mt(px(-KNOB / 2.0))
304}
305
306fn strip_knob(at: f32) -> gpui::Div {
308 div()
309 .absolute()
310 .top_0()
311 .bottom_0()
312 .left_0()
313 .right(px(KNOB))
314 .child(knob_face().top_0().left(gpui::relative(at)))
315}
316
317fn strip() -> gpui::Div {
318 div().w_full().h(px(STRIP_HEIGHT)).rounded_full().flex()
319}
320
321impl Render for ColorPicker {
322 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
323 let theme = Theme::of(cx).clone();
324 let Hsva { h, s, v, a } = self.hsva;
325 let pure = Hsva {
326 h,
327 s: 1.0,
328 v: 1.0,
329 a: 1.0,
330 }
331 .to_hsla();
332 let opaque = Hsva {
333 a: 1.0,
334 ..self.hsva
335 }
336 .to_hsla();
337
338 let field = div()
341 .w_full()
342 .h(px(FIELD_HEIGHT))
343 .bg(linear_gradient(
344 90.0,
345 linear_color_stop(gpui::white(), 0.0),
346 linear_color_stop(pure, 1.0),
347 ))
348 .child(div().absolute().size_full().bg(linear_gradient(
349 180.0,
350 linear_color_stop(gpui::black().opacity(0.0), 0.0),
351 linear_color_stop(gpui::black(), 1.0),
352 )))
353 .child(field_knob(s, 1.0 - v));
354
355 let hue_at = |at: f32| {
357 Hsva {
358 h: at,
359 s: 1.0,
360 v: 1.0,
361 a: 1.0,
362 }
363 .to_hsla()
364 };
365 let hue = strip()
366 .children((0..6).map(|segment| {
367 let from = segment as f32 / 6.0;
368 div()
369 .flex_1()
370 .h_full()
371 .when(segment == 0, |div| div.rounded_l_full())
372 .when(segment == 5, |div| div.rounded_r_full())
373 .bg(linear_gradient(
374 90.0,
375 linear_color_stop(hue_at(from), 0.0),
376 linear_color_stop(hue_at(from + 1.0 / 6.0), 1.0),
377 ))
378 }))
379 .child(strip_knob(h));
380
381 let alpha = self.alpha.then(|| {
382 let strip = strip()
383 .bg(theme.ink(0.12))
384 .child(
385 div()
386 .absolute()
387 .size_full()
388 .rounded_full()
389 .bg(linear_gradient(
390 90.0,
391 linear_color_stop(opaque.opacity(0.0), 0.0),
392 linear_color_stop(opaque, 1.0),
393 )),
394 )
395 .child(strip_knob(a));
396 self.area(Area::Alpha, strip, cx)
397 });
398
399 stack::column()
400 .w_full()
401 .gap(px(8.0))
402 .child(self.area(Area::Field, field, cx))
403 .child(self.area(Area::Hue, hue, cx))
404 .children(alpha)
405 .child(
406 stack::row()
407 .gap(px(8.0))
408 .items_center()
409 .child(
410 div()
411 .flex_none()
412 .size(px(20.0))
413 .rounded_full()
414 .border_1()
415 .border_color(theme.ink(0.25))
416 .bg(self.color()),
417 )
418 .child(
419 div()
420 .flex_1()
421 .min_w_0()
422 .text_style(TextStyle::Callout)
423 .font_family(theme.font_mono.clone())
424 .child(self.hex.clone()),
425 ),
426 )
427 }
428}