Skip to main content

sequoia_elements/
color_picker.rs

1use sequoia_core::{BrushStop, Color, Fill, Length, Point, Rect};
2use sequoia_input::{CursorIcon, Event, PointerButton, PointerEvent};
3use sequoia_layout::{Align, LayoutStyle};
4use sequoia_reactive::{EffectScope, Environment, Signal, batch, effect};
5use sequoia_style::VisualState;
6use sequoia_view::{
7    AnyView, Canvas, Column, EventCtx, Orientation, Row, Spacer, Text, Unit, View, Wrap,
8};
9
10use crate::button::Button;
11use crate::clipboard::SharedClipboard;
12use crate::container::Container;
13use crate::context_menu::ContextMenu;
14use crate::drag_value::DragValue;
15use crate::dropdown::DropdownButton;
16use crate::floating_menu::FloatingMenu;
17use crate::slider1d::Slider1D;
18use crate::svpad::SvPad;
19use crate::text_field::TextField;
20use crate::toast::ToastQueue;
21use std::cell::Cell;
22use std::cell::RefCell;
23use std::rc::Rc;
24use std::time::{Duration, Instant};
25
26const BG_PANEL: Color = Color::rgb8(0x26, 0x26, 0x26);
27const BG_HEADER: Color = Color::rgb8(0x1E, 0x1E, 0x1E);
28const BG_INPUT: Color = Color::rgb8(0x2A, 0x2A, 0x2A);
29const BG_SECTION: Color = Color::rgb8(0x30, 0x30, 0x30);
30const BORDER_PANEL: Color = Color::rgb8(0x00, 0x00, 0x00);
31const BORDER_HAIRLINE: Color = Color::rgb8(0x1F, 0x1F, 0x1F);
32const BORDER_CONTROL: Color = Color::rgb8(0x45, 0x45, 0x45);
33const HAIR_TRACK: Color = Color::rgb8(0x1A, 0x1A, 0x1A);
34const TEXT_TITLE: Color = Color::rgb8(0xED, 0xED, 0xED);
35const TEXT_PRIMARY: Color = Color::rgb8(0xD4, 0xD4, 0xD4);
36const TEXT_SECONDARY: Color = Color::rgb8(0x9A, 0x9A, 0x9A);
37const ACCENT_ACTIVE: Color = Color::rgb8(0xE8, 0x84, 0x2C);
38
39pub const PIPETTE_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M10.763 8.177a2 2 0 0 1-.263.323l-4.621 4.621A3 3 0 0 1 3.758 14H3l-.793.793a.708.708 0 0 1-1-1L2 13v-.758a3 3 0 0 1 .879-2.12L7.5 5.5q.151-.15.322-.264zM12.879 1a2.121 2.121 0 0 1 1.5 3.621L12.5 6.5l.5.5a.707.707 0 0 1-1 1L8 4a.707.707 0 0 1 1-1l.5.5l1.879-1.879a2.12 2.12 0 0 1 1.5-.621"/></svg>"#;
40
41const DEFAULT_SWATCHES: [&str; 8] = [
42    "E5484D", "F76B15", "FFC53D", "46A758", "12A594", "35C2F1", "6E56CF", "E93D82",
43];
44
45const DEFAULT_H: f32 = 201.0;
46const DEFAULT_S: f32 = 0.52;
47const DEFAULT_V: f32 = 0.92;
48const HIST_LIMIT: usize = 100;
49const HIST_WINDOW: Duration = Duration::from_millis(800);
50
51fn clampf(v: f32, lo: f32, hi: f32) -> f32 {
52    v.clamp(lo, hi)
53}
54
55fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
56    let h = h.rem_euclid(360.0);
57    let f = |n: f32| {
58        let k = (n + h / 60.0) % 6.0;
59        v - v * s * 0f32.max((k.min(4.0 - k)).min(1.0))
60    };
61    let q = |c: f32| (clampf(c, 0.0, 1.0) * 255.0).round() as u8;
62    (q(f(5.0)), q(f(3.0)), q(f(1.0)))
63}
64
65fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
66    let (r, g, b) = (r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
67    let mx = r.max(g).max(b);
68    let mn = r.min(g).min(b);
69    let d = mx - mn;
70    let mut h = 0.0;
71    if d > 0.0 {
72        if mx == r {
73            h = ((g - b) / d) % 6.0;
74        } else if mx == g {
75            h = (b - r) / d + 2.0;
76        } else {
77            h = (r - g) / d + 4.0;
78        }
79        h *= 60.0;
80        if h < 0.0 {
81            h += 360.0;
82        }
83    }
84    (h, if mx > 0.0 { d / mx } else { 0.0 }, mx)
85}
86
87fn rgb_to_hsl(r: u8, g: u8, b: u8) -> (f32, f32, f32) {
88    let (r, g, b) = (r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0);
89    let mx = r.max(g).max(b);
90    let mn = r.min(g).min(b);
91    let d = mx - mn;
92    let l = (mx + mn) / 2.0;
93    let mut h = 0.0;
94    let mut s = 0.0;
95    if d > 0.0 {
96        s = d / (1.0 - (2.0 * l - 1.0).abs());
97        if mx == r {
98            h = ((g - b) / d) % 6.0;
99        } else if mx == g {
100            h = (b - r) / d + 2.0;
101        } else {
102            h = (r - g) / d + 4.0;
103        }
104        h *= 60.0;
105        if h < 0.0 {
106            h += 360.0;
107        }
108    }
109    (h, s, l)
110}
111
112fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
113    let h = h.rem_euclid(360.0);
114    let a = s * l.min(1.0 - l);
115    let f = |n: f32| {
116        let k = (n + h / 30.0) % 12.0;
117        l - a * (-1f32).max((k - 3.0).min(9.0 - k).min(1.0))
118    };
119    let q = |c: f32| (clampf(c, 0.0, 1.0) * 255.0).round() as u8;
120    (q(f(0.0)), q(f(8.0)), q(f(4.0)))
121}
122
123fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
124    format!("{r:02X}{g:02X}{b:02X}")
125}
126
127fn parse_hex(s: &str) -> Option<String> {
128    let mut clean: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
129    if clean.len() == 3 {
130        clean = clean.chars().flat_map(|c| [c, c]).collect::<String>();
131    }
132    if clean.len() == 6 {
133        Some(clean.to_uppercase())
134    } else {
135        None
136    }
137}
138
139fn hex_to_rgb(hx: &str) -> (u8, u8, u8) {
140    let n = |i: usize| u8::from_str_radix(&hx[i..i + 2], 16).unwrap_or(0);
141    (n(0), n(2), n(4))
142}
143
144#[allow(dead_code)]
145fn fmt_val(v: f32, max: f32) -> String {
146    if max == 255.0 || max == 360.0 {
147        return format!("{}", v.round() as i32);
148    }
149    let r = v.round();
150    if (v - r).abs() < 0.05 {
151        format!("{r}")
152    } else {
153        format!("{:.1}", (v * 10.0).round() / 10.0)
154    }
155}
156
157struct Rec {
158    last_hex: String,
159    last_push: Option<Instant>,
160    in_history: bool,
161}
162
163#[derive(Clone, Copy)]
164struct PickerDrag {
165    origin: Point,
166    start: (f32, f32),
167}
168
169#[derive(Clone)]
170pub struct ColorPicker {
171    hue: Signal<f32>,
172    sat: Signal<f32>,
173    val: Signal<f32>,
174    model: Signal<usize>,
175    ref_hex: Signal<String>,
176    hex_text: Signal<String>,
177    editing_hex: Signal<bool>,
178    swatches: Signal<Vec<String>>,
179    undo: Signal<Vec<String>>,
180    redo: Signal<Vec<String>>,
181    active: Signal<Option<usize>>,
182    closed: Signal<bool>,
183    ch: [Signal<f32>; 3],
184    toasts: ToastQueue,
185    rec: Rc<RefCell<Rec>>,
186    on_close: Rc<RefCell<Option<Rc<dyn Fn()>>>>,
187
188    pos: Signal<(f32, f32)>,
189    drag: Rc<Cell<Option<PickerDrag>>>,
190}
191
192pub type Picker = ColorPicker;
193
194impl Default for ColorPicker {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200impl ColorPicker {
201    pub fn new() -> Self {
202        let (h, s, v) = (DEFAULT_H, DEFAULT_S, DEFAULT_V);
203        let (r, g, b) = hsv_to_rgb(h, s, v);
204        let start_hex = rgb_to_hex(r, g, b);
205        let p = Self {
206            hue: Signal::new(h),
207            sat: Signal::new(s),
208            val: Signal::new(v),
209            model: Signal::new(0),
210            ref_hex: Signal::new(start_hex.clone()),
211            hex_text: Signal::new(start_hex.clone()),
212            editing_hex: Signal::new(false),
213            swatches: Signal::new(DEFAULT_SWATCHES.iter().map(|s| s.to_string()).collect()),
214            undo: Signal::new(Vec::new()),
215            redo: Signal::new(Vec::new()),
216            active: Signal::new(None),
217            closed: Signal::new(false),
218            ch: [Signal::new(0.0), Signal::new(0.0), Signal::new(0.0)],
219            toasts: ToastQueue::new(),
220            rec: Rc::new(RefCell::new(Rec {
221                last_hex: start_hex,
222                last_push: None,
223                in_history: false,
224            })),
225            on_close: Rc::new(RefCell::new(None)),
226            pos: Signal::new((90.0, 70.0)),
227            drag: Rc::new(Cell::new(None)),
228        };
229        p.sync_channels();
230        p
231    }
232
233    pub fn set_hex(&self, hx: &str) {
234        self.apply_hex(hx);
235    }
236
237    pub fn hsv_signals(&self) -> (Signal<f32>, Signal<f32>, Signal<f32>) {
238        (self.hue.clone(), self.sat.clone(), self.val.clone())
239    }
240
241    pub fn toasts(&self) -> ToastQueue {
242        self.toasts.clone()
243    }
244
245    pub fn pos_signal(&self) -> Signal<(f32, f32)> {
246        self.pos.clone()
247    }
248
249    pub fn set_on_close(&self, f: impl Fn() + 'static) {
250        *self.on_close.borrow_mut() = Some(Rc::new(f));
251    }
252
253    fn close(&self) {
254        if let Some(f) = self.on_close.borrow().clone() {
255            f();
256        } else {
257            self.closed.set(true);
258        }
259    }
260
261    pub fn cur_rgb(&self) -> (u8, u8, u8) {
262        hsv_to_rgb(
263            self.hue.get_untracked(),
264            self.sat.get_untracked(),
265            self.val.get_untracked(),
266        )
267    }
268
269    pub fn cur_hex(&self) -> String {
270        let (r, g, b) = self.cur_rgb();
271        rgb_to_hex(r, g, b)
272    }
273
274    pub fn cur_color(&self) -> Color {
275        let (r, g, b) = self.cur_rgb();
276        Color::rgb8(r, g, b)
277    }
278
279    fn set_rgb(&self, r: u8, g: u8, b: u8) {
280        let (nh, ns, nv) = rgb_to_hsv(r, g, b);
281        batch(|| {
282            if nv > 0.0001 {
283                if ns > 0.0001 {
284                    self.hue.set(nh);
285                }
286                self.sat.set(ns);
287            }
288            self.val.set(nv);
289        });
290    }
291
292    fn apply_hex(&self, hx: &str) {
293        if let Some(n) = parse_hex(hx) {
294            let (r, g, b) = hex_to_rgb(&n);
295            self.set_rgb(r, g, b);
296        }
297    }
298
299    fn channel_value(&self, idx: usize) -> f32 {
300        match self.model.get_untracked() {
301            0 => {
302                let (r, g, b) = self.cur_rgb();
303                match idx {
304                    0 => r as f32,
305                    1 => g as f32,
306                    _ => b as f32,
307                }
308            }
309            1 => match idx {
310                0 => self.hue.get_untracked(),
311                1 => self.sat.get_untracked() * 100.0,
312                _ => self.val.get_untracked() * 100.0,
313            },
314            _ => {
315                let (r, g, b) = self.cur_rgb();
316                let (h, s, l) = rgb_to_hsl(r, g, b);
317                let _ = h;
318                match idx {
319                    0 => self.hue.get_untracked(),
320                    1 => s * 100.0,
321                    _ => l * 100.0,
322                }
323            }
324        }
325    }
326
327    fn apply_channel(&self, idx: usize, v: f32) {
328        batch(|| match self.model.get_untracked() {
329            0 => {
330                let (r, g, b) = self.cur_rgb();
331                let q = clampf(v, 0.0, 255.0).round() as u8;
332                match idx {
333                    0 => self.set_rgb(q, g, b),
334                    1 => self.set_rgb(r, q, b),
335                    _ => self.set_rgb(r, g, q),
336                }
337            }
338            1 => match idx {
339                0 => self.hue.set(v.rem_euclid(360.0)),
340                1 => self.sat.set(clampf(v, 0.0, 100.0) / 100.0),
341                _ => self.val.set(clampf(v, 0.0, 100.0) / 100.0),
342            },
343            _ => {
344                if idx == 0 {
345                    self.hue.set(v.rem_euclid(360.0));
346                } else {
347                    let (r, g, b) = self.cur_rgb();
348                    let (hh, mut ss, mut ll) = rgb_to_hsl(r, g, b);
349                    if idx == 1 {
350                        ss = clampf(v, 0.0, 100.0) / 100.0;
351                    } else {
352                        ll = clampf(v, 0.0, 100.0) / 100.0;
353                    }
354                    let (nr, ng, nb) = hsl_to_rgb(hh, ss, ll);
355                    self.set_rgb(nr, ng, nb);
356                }
357            }
358        });
359    }
360
361    fn sync_channels(&self) {
362        for i in 0..3 {
363            let want = self.channel_value(i);
364            if (self.ch[i].get_untracked() - want).abs() > 1e-4 {
365                self.ch[i].set(want);
366            }
367        }
368        if !self.editing_hex.get_untracked() {
369            let hx = self.cur_hex();
370            if self.hex_text.get_untracked() != hx {
371                self.hex_text.set(hx);
372            }
373        }
374    }
375
376    fn note_change(&self, new_hex: String) {
377        let mut r = self.rec.borrow_mut();
378        if r.in_history || new_hex == r.last_hex {
379            return;
380        }
381        let now = Instant::now();
382        let coalesce = r
383            .last_push
384            .map(|t| now.duration_since(t) < HIST_WINDOW)
385            .unwrap_or(false);
386        if !coalesce {
387            let prev = r.last_hex.clone();
388            self.undo.update(|u| {
389                if u.last() != Some(&prev) {
390                    u.push(prev);
391                    if u.len() > HIST_LIMIT {
392                        u.remove(0);
393                    }
394                }
395            });
396            self.redo.set(Vec::new());
397        }
398        r.last_push = Some(now);
399        r.last_hex = new_hex;
400    }
401
402    pub fn do_undo(&self) {
403        let mut u = self.undo.get_untracked();
404        let Some(prev) = u.pop() else {
405            return;
406        };
407        let cur = self.cur_hex();
408        self.redo.update(|r| r.push(cur));
409        self.undo.set(u);
410        self.rec.borrow_mut().in_history = true;
411        self.apply_hex(&prev);
412        self.sync_channels();
413        let mut r = self.rec.borrow_mut();
414        r.last_hex = prev;
415        r.last_push = None;
416        r.in_history = false;
417    }
418
419    pub fn do_redo(&self) {
420        let mut rd = self.redo.get_untracked();
421        let Some(next) = rd.pop() else {
422            return;
423        };
424        let cur = self.cur_hex();
425        self.undo.update(|u| u.push(cur));
426        self.redo.set(rd);
427        self.rec.borrow_mut().in_history = true;
428        self.apply_hex(&next);
429        self.sync_channels();
430        let mut r = self.rec.borrow_mut();
431        r.last_hex = next;
432        r.last_push = None;
433        r.in_history = false;
434    }
435
436    pub fn set_reference(&self) {
437        self.ref_hex.set(self.cur_hex());
438        self.toast("Reference color set");
439    }
440
441    fn revert(&self) {
442        let rf = self.ref_hex.get_untracked();
443        if rf == self.cur_hex() {
444            return;
445        }
446        self.apply_hex(&rf);
447        self.toast("Reverted to reference");
448    }
449
450    fn with_clip(&self, env: &Environment, f: impl FnOnce(&SharedClipboard)) -> bool {
451        match env.get::<SharedClipboard>() {
452            Some(c) => {
453                f(&c);
454                true
455            }
456            None => false,
457        }
458    }
459
460    fn toast(&self, msg: impl Into<String>) {
461        self.toasts.info(msg, "");
462    }
463
464    fn copy_hex(&self, env: &Environment) {
465        let t = format!("#{}", self.cur_hex());
466        if self.with_clip(env, |c| c.0.set_text(&t)) {
467            self.toast(format!("Copied {t}"));
468        } else {
469            self.toast("Clipboard unavailable");
470        }
471    }
472
473    fn copy_rgb(&self, env: &Environment) {
474        let (r, g, b) = self.cur_rgb();
475        let t = format!("rgb({r}, {g}, {b})");
476        if self.with_clip(env, |c| c.0.set_text(&t)) {
477            self.toast(format!("Copied {t}"));
478        } else {
479            self.toast("Clipboard unavailable");
480        }
481    }
482
483    fn copy_hsl(&self, env: &Environment) {
484        let (r, g, b) = self.cur_rgb();
485        let (h, s, l) = rgb_to_hsl(r, g, b);
486        let t = format!(
487            "hsl({}, {}%, {}%)",
488            h.round() as i32,
489            (s * 100.0).round() as i32,
490            (l * 100.0).round() as i32
491        );
492        if self.with_clip(env, |c| c.0.set_text(&t)) {
493            self.toast(format!("Copied {t}"));
494        } else {
495            self.toast("Clipboard unavailable");
496        }
497    }
498
499    fn paste(&self, env: &Environment) {
500        let Some(clip) = env.get::<SharedClipboard>() else {
501            self.toast("Clipboard access denied");
502            return;
503        };
504        let Some(txt) = clip.0.get_text() else {
505            self.toast("Clipboard access denied");
506            return;
507        };
508        let txt = txt.trim().to_string();
509        if let Some(hx) = parse_hex(&txt) {
510            self.apply_hex(&hx);
511            self.toast(format!("Pasted #{hx}"));
512            return;
513        }
514        let low = txt.to_lowercase();
515        if low.contains("rgb") {
516            let nums: Vec<u8> = low
517                .split(|c: char| !c.is_ascii_digit())
518                .filter(|s| !s.is_empty())
519                .filter_map(|s| s.parse::<u16>().ok())
520                .map(|n| n.min(255) as u8)
521                .collect();
522            if nums.len() >= 3 {
523                self.set_rgb(nums[0], nums[1], nums[2]);
524                self.toast(format!("Pasted #{}", self.cur_hex()));
525                return;
526            }
527        }
528        self.toast("No color found in clipboard");
529    }
530
531    fn sw_add(&self) {
532        let hx = self.cur_hex();
533        let mut sw = self.swatches.get_untracked();
534        if sw.contains(&hx) {
535            self.toast(format!("#{hx} is already in swatches"));
536            return;
537        }
538        sw.push(hx);
539        self.swatches.set(sw);
540    }
541
542    fn sw_sort(&self) {
543        let mut sw = self.swatches.get_untracked();
544        sw.sort_by(|a, b| {
545            let (r1, g1, b1) = hex_to_rgb(a);
546            let (r2, g2, b2) = hex_to_rgb(b);
547            let (h1, s1, v1) = rgb_to_hsv(r1, g1, b1);
548            let (h2, s2, v2) = rgb_to_hsv(r2, g2, b2);
549            let _ = (s1, v1, s2, v2);
550            let g1 = if s1 < 0.06 { 999.0 } else { h1 };
551            let g2 = if s2 < 0.06 { 999.0 } else { h2 };
552            g1.partial_cmp(&g2)
553                .unwrap_or(std::cmp::Ordering::Equal)
554                .then(v2.partial_cmp(&v1).unwrap_or(std::cmp::Ordering::Equal))
555        });
556        self.swatches.set(sw);
557    }
558
559    pub fn install_effects(&self) {
560        self.rec.borrow_mut().last_hex = self.cur_hex();
561
562        let p = self.clone();
563        let s1 = effect(move || {
564            let _ = (p.hue.get(), p.sat.get(), p.val.get(), p.model.get());
565            p.note_change(p.cur_hex());
566            p.sync_channels();
567        });
568
569        let p = self.clone();
570        let s2 = effect(move || {
571            let cur = [p.ch[0].get(), p.ch[1].get(), p.ch[2].get()];
572            let want = [p.channel_value(0), p.channel_value(1), p.channel_value(2)];
573            let eps = if p.model.get_untracked() == 0 {
574                0.51
575            } else {
576                0.02
577            };
578            let mut hit = None;
579            for i in 0..3 {
580                if (cur[i] - want[i]).abs() > eps {
581                    hit = Some(i);
582                    break;
583                }
584            }
585            if let Some(i) = hit {
586                p.active.set(Some(i));
587                p.apply_channel(i, cur[i]);
588            }
589        });
590
591        let p = self.clone();
592        let s3 = effect(move || {
593            let t = p.hex_text.get();
594            if t.len() == 6
595                && let Some(n) = parse_hex(&t)
596                && n != p.cur_hex()
597            {
598                p.apply_hex(&n);
599            }
600        });
601
602        {
603            let mut scope = EffectScope::new();
604            scope.extend([s1, s2, s3]);
605            scope.leak();
606        }
607    }
608}
609
610fn channel_meta(model: usize, idx: usize) -> (&'static str, f32, f32, usize) {
611    match model {
612        0 => match idx {
613            0 => ("R", 0.0, 255.0, 0),
614            1 => ("G", 0.0, 255.0, 0),
615            _ => ("B", 0.0, 255.0, 0),
616        },
617        1 => match idx {
618            0 => ("H", 0.0, 360.0, 0),
619            1 => ("S", 0.0, 100.0, 1),
620            _ => ("V", 0.0, 100.0, 1),
621        },
622        _ => match idx {
623            0 => ("H", 0.0, 360.0, 0),
624            1 => ("S", 0.0, 100.0, 1),
625            _ => ("L", 0.0, 100.0, 1),
626        },
627    }
628}
629
630fn hue_stops() -> Vec<BrushStop> {
631    (0..=6)
632        .map(|i| {
633            BrushStop::new(
634                i as f32 / 6.0,
635                Color::from_hsva(i as f32 / 6.0, 1.0, 1.0, 1.0),
636            )
637        })
638        .collect()
639}
640
641fn channel_stops(p: &ColorPicker, idx: usize) -> Vec<BrushStop> {
642    let h = p.hue.get_untracked();
643    let s = p.sat.get_untracked();
644    let v = p.val.get_untracked();
645    let (r, g, b) = hsv_to_rgb(h, s, v);
646    match p.model.get_untracked() {
647        0 => {
648            let (a, c) = match idx {
649                0 => (Color::rgb8(0, g, b), Color::rgb8(255, g, b)),
650                1 => (Color::rgb8(r, 0, b), Color::rgb8(r, 255, b)),
651                _ => (Color::rgb8(r, g, 0), Color::rgb8(r, g, 255)),
652            };
653            vec![BrushStop::new(0.0, a), BrushStop::new(1.0, c)]
654        }
655        1 => match idx {
656            0 => hue_stops(),
657            1 => vec![
658                BrushStop::new(0.0, Color::from_hsva(h / 360.0, 0.0, v, 1.0)),
659                BrushStop::new(1.0, Color::from_hsva(h / 360.0, 1.0, v, 1.0)),
660            ],
661            _ => vec![
662                BrushStop::new(0.0, Color::BLACK),
663                BrushStop::new(1.0, Color::from_hsva(h / 360.0, s, 1.0, 1.0)),
664            ],
665        },
666        _ => {
667            let (_, _, l) = rgb_to_hsl(r, g, b);
668            match idx {
669                0 => hue_stops(),
670                1 => vec![
671                    BrushStop::new(0.0, Color::from_hsl(h, 0.0, l)),
672                    BrushStop::new(1.0, Color::from_hsl(h, 1.0, l)),
673                ],
674                _ => vec![
675                    BrushStop::new(0.0, Color::BLACK),
676                    BrushStop::new(0.5, Color::from_hsl(h, 1.0, 0.5)),
677                    BrushStop::new(1.0, Color::WHITE),
678                ],
679            }
680        }
681    }
682}
683
684#[derive(Clone)]
685struct ChanRow {
686    p: ColorPicker,
687    idx: usize,
688}
689
690impl View for ChanRow {
691    fn body(&self) -> impl View + 'static {
692        let m = self.p.model.get();
693        let _ = (self.p.hue.get(), self.p.sat.get(), self.p.val.get());
694        let act = self.p.active.get();
695        let (label, min, max, prec) = channel_meta(m, self.idx);
696        let stops = channel_stops(&self.p, self.idx);
697        let sig = self.p.ch[self.idx].clone();
698        let sig2 = sig.clone();
699        let anchored = act == Some(self.idx);
700
701        let pc = self.p.clone();
702        let idx = self.idx;
703        let track = Slider1D::new(sig, min, max)
704            .gradient(stops, Orientation::Horizontal)
705            .track_height(8.0)
706            .on_commit(move |val| {
707                pc.active.set(Some(idx));
708                pc.apply_channel(idx, val);
709            });
710
711        let pc = self.p.clone();
712        let idx = self.idx;
713        let mut value = DragValue::new(sig2)
714            .min(min)
715            .max(max)
716            .precision(prec)
717            .width(52.0)
718            .line_full(true)
719            .on_commit(move |val| {
720                pc.active.set(Some(idx));
721                pc.apply_channel(idx, val);
722            });
723        if anchored {
724            value = value.line_color(ACCENT_ACTIVE).line_width(2.0);
725        }
726
727        Row::new()
728            .gap(8.0)
729            .align_items(Align::Center)
730            .child(
731                Text::new(label)
732                    .size(11.0)
733                    .color(if anchored {
734                        TEXT_PRIMARY
735                    } else {
736                        TEXT_SECONDARY
737                    })
738                    .width(11.0),
739            )
740            .child_flex(track, 1.0)
741            .child(value)
742    }
743}
744
745#[derive(Clone)]
746struct PreviewBox {
747    p: ColorPicker,
748}
749
750impl PreviewBox {
751    const W: f32 = 64.0;
752    const H: f32 = 24.0;
753
754    const SPLIT: f32 = Self::W * 1.6 / 2.6;
755}
756
757impl View for PreviewBox {
758    fn body(&self) -> impl View + 'static {
759        let pd = self.p.clone();
760        let pc = self.p.clone();
761        let pt = self.p.clone();
762        let armed = Rc::new(Cell::new(false));
763        let armed_up = armed.clone();
764        Container::new()
765            .width(Self::W)
766            .height(Self::H)
767            .border(1.0, HAIR_TRACK)
768            .radius(2.0)
769            .clip(true)
770            .child(
771                Canvas::new()
772                    .cursor(CursorIcon::Pointer)
773                    .on_draw(move |g| {
774                        let new_c = pd.cur_color();
775                        let ref_c = Color::parse_hex(&pd.ref_hex.get_untracked()).unwrap_or(new_c);
776                        g.fill(
777                            Rect::from_xywh(0.0, 0.0, Self::SPLIT + 1.0, Self::H),
778                            Fill::solid(new_c),
779                        );
780                        g.fill(
781                            Rect::from_xywh(Self::SPLIT, 0.0, Self::W - Self::SPLIT, Self::H),
782                            Fill::solid(ref_c),
783                        );
784                    })
785                    .on_pointer(
786                        move |pos: Point, ev: &PointerEvent, ctx: &mut EventCtx| match ev {
787                            PointerEvent::Down {
788                                button: PointerButton::Left,
789                                ..
790                            } => {
791                                armed.set(pos.x >= Self::SPLIT);
792                                if armed.get() {
793                                    ctx.capture_pointer();
794                                    ctx.handled();
795                                }
796                            }
797                            PointerEvent::Up {
798                                button: PointerButton::Left,
799                                ..
800                            } => {
801                                if armed_up.take() && pos.x >= Self::SPLIT {
802                                    pc.revert();
803                                }
804                                ctx.handled();
805                            }
806                            _ => {}
807                        },
808                    )
809                    .track(move || {
810                        pt.hue.get();
811                        pt.sat.get();
812                        pt.val.get();
813                        pt.ref_hex.get();
814                    }),
815            )
816    }
817}
818
819#[derive(Clone)]
820struct SwatchBox {
821    p: ColorPicker,
822    hx: String,
823}
824
825impl View for SwatchBox {
826    fn body(&self) -> impl View + 'static {
827        let _ = (self.p.hue.get(), self.p.sat.get(), self.p.val.get());
828        let selected = self.p.cur_hex() == self.hx;
829        let bg = Color::parse_hex(&self.hx).unwrap_or(Color::BLACK);
830        let pa = self.p.clone();
831        let hx = self.hx.clone();
832        let cell = Container::new()
833            .width(22.0)
834            .height(22.0)
835            .background(bg)
836            .border(
837                1.0,
838                if selected {
839                    Color::rgb8(0x35, 0xC2, 0xF1)
840                } else {
841                    HAIR_TRACK
842                },
843            )
844            .radius(2.0)
845            .on_click(move || pa.apply_hex(&hx));
846        let po = self.p.clone();
847        let hx_a = self.hx.clone();
848        let hx2 = self.hx.clone();
849        let hx3 = self.hx.clone();
850        let hx_d = self.hx.clone();
851        ContextMenu::new(cell)
852            .item("Zastosuj", move || po.apply_hex(&hx_a))
853            .item("Overwrite with current", {
854                let po = self.p.clone();
855                move || {
856                    let cur = po.cur_hex();
857                    po.swatches.update(|sw| {
858                        if let Some(slot) = sw.iter_mut().find(|s| *s == &hx2) {
859                            *slot = cur;
860                        }
861                    });
862                }
863            })
864            .item_env("Kopiuj HEX", {
865                let po = self.p.clone();
866                move |env: &Environment| {
867                    let t = format!("#{hx3}");
868                    match env.get::<SharedClipboard>() {
869                        Some(c) => {
870                            c.0.set_text(&t);
871                            po.toast(format!("Copied {t}"));
872                        }
873                        None => po.toast("Clipboard unavailable"),
874                    }
875                }
876            })
877            .item("Delete", {
878                let po = self.p.clone();
879                move || {
880                    po.swatches.update(|sw| sw.retain(|s| *s != hx_d));
881                }
882            })
883    }
884}
885
886#[derive(Clone)]
887struct SwatchesView {
888    p: ColorPicker,
889}
890
891impl View for SwatchesView {
892    fn body(&self) -> impl View + 'static {
893        let sw = self.p.swatches.get();
894        let _ = (self.p.hue.get(), self.p.sat.get(), self.p.val.get());
895        let mut wrap = Wrap::new()
896            .gap(6.0)
897            .padding_xy(10.0, 0.0)
898            .padding_bottom(10.0);
899        let pa = self.p.clone();
900        wrap = wrap.child(
901            Button::icon("plus")
902                .icon_size(12.0)
903                .padding(5.0, 5.0)
904                .on_click(move || pa.sw_add()),
905        );
906        for hx in sw {
907            wrap = wrap.child(SwatchBox {
908                p: self.p.clone(),
909                hx,
910            });
911        }
912        wrap
913    }
914}
915
916impl ColorPicker {
917    pub fn panel(&self) -> impl View + 'static {
918        let p = self.clone();
919        let title = Text::with(move || {
920            let dirty = p.cur_hex() != p.ref_hex.get();
921            format!("BASE COLOR{}", if dirty { " *" } else { "" })
922        })
923        .size(11.0)
924        .bold()
925        .color(TEXT_TITLE);
926
927        let pc = self.clone();
928        let header = Container::new()
929            .fill_width()
930            .height(34.0)
931            .background(BG_HEADER)
932            .border_bottom(1.0, BORDER_PANEL)
933            .padding_xy(10.0, 0.0)
934            .child(
935                Row::new()
936                    .fill_size()
937                    .gap(8.0)
938                    .align_items(Align::Center)
939                    .child_flex(title, 1.0)
940                    .child(
941                        Button::icon("close")
942                            .icon_size(12.0)
943                            .padding_xy(8.0, 7.0)
944                            .on_click(move || pc.close()),
945                    ),
946            );
947
948        let picker_row = Row::new()
949            .gap(8.0)
950            .padding_xy(10.0, 10.0)
951            .child_flex(
952                SvPad::new(self.sat.clone(), self.val.clone(), self.hue.clone()),
953                1.0,
954            )
955            .child(
956                Slider1D::vertical(self.hue.clone(), 0.0, 360.0)
957                    .gradient(hue_stops(), Orientation::Vertical)
958                    .track_height(8.0)
959                    .width(14.0),
960            );
961
962        let pp = self.clone();
963        let hex_field = TextField::new(self.hex_text.clone())
964            .max_length(6)
965            .width(88.0)
966            .height(26.0)
967            .font_size(12.0)
968            .padding_xy(7.0, 0.0)
969            .background(BG_INPUT)
970            .border(1.0, BORDER_CONTROL)
971            .radius(3.0)
972            .on_edit_start({
973                let pe = self.clone();
974                move || pe.editing_hex.set(true)
975            })
976            .on_edit_end({
977                let pe = self.clone();
978                move |t: String| {
979                    pe.editing_hex.set(false);
980                    if parse_hex(&t).is_some() {
981                        pe.apply_hex(&t);
982                    } else {
983                        pe.toast("Invalid hex");
984                    }
985                    pe.sync_channels();
986                }
987            })
988            .on_submit({
989                let pe = self.clone();
990                move |t: String| {
991                    pe.editing_hex.set(false);
992                    if parse_hex(&t).is_some() {
993                        pe.apply_hex(&t);
994                    }
995                    pe.sync_channels();
996                }
997            });
998
999        let info_row = Row::new()
1000            .gap(8.0)
1001            .padding_x(10.0)
1002            .align_items(Align::Center)
1003            .child(PreviewBox { p: self.clone() })
1004            .child(Spacer::flex(1.0))
1005            .child(Text::new("#").size(11.0).color(TEXT_SECONDARY))
1006            .child(hex_field)
1007            .child(Button::icon("pipette").icon_size(16.0).on_click(move || {
1008                pp.toast("Eyedropper unavailable in this build (browser-only API)");
1009            }));
1010
1011        let pm = self.clone();
1012        let model_drop = DropdownButton::new(
1013            self.model.clone(),
1014            vec!["RGB".to_string(), "HSV".to_string(), "HSL".to_string()],
1015        )
1016        .font_size(12.0)
1017        .padding_xy(7.0, 3.0)
1018        .on_select(move |i| {
1019            pm.model.set(i);
1020            pm.active.set(None);
1021            pm.sync_channels();
1022        });
1023        let model_row = Row::new()
1024            .height(26.0)
1025            .padding_x(10.0)
1026            .align_items(Align::Center)
1027            .child(model_drop)
1028            .child(Spacer::flex(1.0))
1029            .child(self.color_menu());
1030
1031        let channels = Column::new()
1032            .gap(4.0)
1033            .padding_x(10.0)
1034            .child(ChanRow {
1035                p: self.clone(),
1036                idx: 0,
1037            })
1038            .child(ChanRow {
1039                p: self.clone(),
1040                idx: 1,
1041            })
1042            .child(ChanRow {
1043                p: self.clone(),
1044                idx: 2,
1045            });
1046
1047        let section = Container::new()
1048            .fill_width()
1049            .height(24.0)
1050            .background(BG_SECTION)
1051            .border_top(1.0, BORDER_HAIRLINE)
1052            .border_bottom(1.0, BORDER_HAIRLINE)
1053            .padding_xy(10.0, 0.0)
1054            .child(
1055                Row::new()
1056                    .fill_size()
1057                    .align_items(Align::Center)
1058                    .child(Text::new("Swatches").size(11.0).color(TEXT_PRIMARY))
1059                    .child(Spacer::flex(1.0))
1060                    .child(self.sw_menu()),
1061            );
1062
1063        let body = Column::new()
1064            .fill_width()
1065            .gap(10.0)
1066            .child(picker_row)
1067            .child(info_row)
1068            .child(model_row)
1069            .child(channels)
1070            .child(section)
1071            .child(SwatchesView { p: self.clone() });
1072
1073        Container::new()
1074            .width(280.0)
1075            .background(BG_PANEL)
1076            .border(1.0, BORDER_PANEL)
1077            .child(Column::new().fill_width().child(header).child(body))
1078    }
1079
1080    fn color_menu(&self) -> impl View + 'static {
1081        let dots = Button::new("···").font_size(11.0).padding_xy(6.0, 2.0);
1082        let p = self.clone();
1083        FloatingMenu::wrap(dots)
1084            .below_invoker()
1085            .open_on_click()
1086            .text_row("Cofnij  Ctrl+Z", {
1087                let p = p.clone();
1088                move || p.do_undo()
1089            })
1090            .text_row("Redo Ctrl+Y", {
1091                let p = p.clone();
1092                move || p.do_redo()
1093            })
1094            .separator()
1095            .text_row_env("Kopiuj HEX", {
1096                let p = p.clone();
1097                move |env: &Environment| p.copy_hex(env)
1098            })
1099            .text_row_env("Kopiuj RGB", {
1100                let p = p.clone();
1101                move |env: &Environment| p.copy_rgb(env)
1102            })
1103            .text_row_env("Kopiuj HSL", {
1104                let p = p.clone();
1105                move |env: &Environment| p.copy_hsl(env)
1106            })
1107            .separator()
1108            .text_row_env("Wklej  Ctrl+Shift+V", {
1109                let p = p.clone();
1110                move |env: &Environment| p.paste(env)
1111            })
1112            .text_row("Pipeta  E", {
1113                let p = p.clone();
1114                move || p.toast("Eyedropper unavailable in this build (browser-only API)")
1115            })
1116            .separator()
1117            .text_row("Ustaw jako odniesienie  Ctrl+S", {
1118                let p = p.clone();
1119                move || p.set_reference()
1120            })
1121    }
1122
1123    fn sw_menu(&self) -> impl View + 'static {
1124        let dots = Button::new("···").font_size(11.0).padding_xy(6.0, 2.0);
1125        let p = self.clone();
1126        FloatingMenu::wrap(dots)
1127            .below_invoker()
1128            .open_on_click()
1129            .text_row("Add Current Color", {
1130                let p = p.clone();
1131                move || p.sw_add()
1132            })
1133            .text_row("Sortuj wg odcienia", {
1134                let p = p.clone();
1135                move || p.sw_sort()
1136            })
1137            .separator()
1138            .text_row("Restore Defaults", {
1139                let p = p.clone();
1140                move || {
1141                    p.swatches
1142                        .set(DEFAULT_SWATCHES.iter().map(|s| s.to_string()).collect());
1143                    p.toast("Default swatches restored");
1144                }
1145            })
1146            .text_row("Clear", {
1147                let p = p.clone();
1148                move || {
1149                    p.swatches.set(Vec::new());
1150                    p.toast("Swatches cleared");
1151                }
1152            })
1153    }
1154
1155    pub fn floating(&self) -> PickerFloat {
1156        PickerFloat {
1157            picker: self.clone(),
1158        }
1159    }
1160}
1161
1162const PICKER_HEADER_H: f32 = 34.0;
1163const PICKER_CLOSE_W: f32 = 40.0;
1164
1165#[derive(Clone)]
1166pub struct PickerFloat {
1167    picker: ColorPicker,
1168}
1169
1170impl View for PickerFloat {
1171    fn body(&self) -> impl View + 'static {
1172        let _ = self.picker.pos.get();
1173        Unit
1174    }
1175
1176    fn children_override(&self) -> Option<Vec<AnyView>> {
1177        Some(vec![AnyView::new(self.picker.panel())])
1178    }
1179
1180    fn layout_style(&self) -> LayoutStyle {
1181        LayoutStyle {
1182            width: Length::Fit,
1183            height: Length::Fit,
1184            ..LayoutStyle::default()
1185        }
1186    }
1187
1188    fn has_visual_state(&self) -> bool {
1189        true
1190    }
1191
1192    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
1193        if self.picker.drag.get().is_some() {
1194            CursorIcon::Grabbing
1195        } else {
1196            CursorIcon::Default
1197        }
1198    }
1199
1200    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
1201        match event {
1202            Event::Pointer(PointerEvent::Down {
1203                button: PointerButton::Left,
1204                pos,
1205                ..
1206            }) => {
1207                let head = Rect::from_xywh(
1208                    ctx.rect.left(),
1209                    ctx.rect.top(),
1210                    (ctx.rect.size.width - PICKER_CLOSE_W).max(0.0),
1211                    PICKER_HEADER_H.min(ctx.rect.size.height),
1212                );
1213                if head.contains(*pos) {
1214                    let (cx, cy) = self.picker.pos.get_untracked();
1215                    self.picker.drag.set(Some(PickerDrag {
1216                        origin: *pos,
1217                        start: (cx, cy),
1218                    }));
1219                    ctx.capture_pointer();
1220                    ctx.handled();
1221                }
1222            }
1223            Event::Pointer(PointerEvent::Move { pos, .. }) => {
1224                if let Some(st) = self.picker.drag.get() {
1225                    let nx = st.start.0 + (pos.x - st.origin.x);
1226                    let ny = st.start.1 + (pos.y - st.origin.y);
1227                    if nx.is_finite() && ny.is_finite() {
1228                        let win = ctx.window_size();
1229                        let max_x = (win.width - 120.0).max(0.0);
1230                        let max_y = (win.height - PICKER_HEADER_H).max(0.0);
1231                        self.picker
1232                            .pos
1233                            .set((nx.clamp(0.0, max_x), ny.clamp(0.0, max_y)));
1234                    }
1235                    ctx.handled();
1236                }
1237            }
1238            Event::Pointer(PointerEvent::Up {
1239                button: PointerButton::Left,
1240                ..
1241            }) => {
1242                if self.picker.drag.take().is_some() {
1243                    ctx.handled();
1244                }
1245            }
1246            Event::Pointer(PointerEvent::Cancel) => {
1247                self.picker.drag.set(None);
1248            }
1249            _ => {}
1250        }
1251    }
1252}