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, Inset, LayoutStyle, Position};
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 = s.trim();
129    if let Some(rest) = clean.strip_prefix('#') {
130        clean = rest;
131    } else if let Some(rest) = clean
132        .strip_prefix("0x")
133        .or_else(|| clean.strip_prefix("0X"))
134    {
135        clean = rest;
136    }
137    if clean.is_empty() || !clean.chars().all(|c| c.is_ascii_hexdigit()) {
138        return None;
139    }
140    let rgb = match clean.len() {
141        3 => clean.chars().flat_map(|c| [c, c]).collect::<String>(),
142        4 => clean[..3].chars().flat_map(|c| [c, c]).collect::<String>(),
143        6 => clean.to_string(),
144        8 => clean[..6].to_string(),
145        _ => return None,
146    };
147    Some(rgb.to_uppercase())
148}
149
150fn hex_to_rgb(hx: &str) -> (u8, u8, u8) {
151    let n = |i: usize| u8::from_str_radix(&hx[i..i + 2], 16).unwrap_or(0);
152    (n(0), n(2), n(4))
153}
154
155fn color_components(s: &str) -> Vec<(f32, bool)> {
156    let bytes = s.as_bytes();
157    let mut out = Vec::new();
158    let mut i = 0;
159    while i < bytes.len() {
160        let negative = bytes[i] == b'-';
161        let start_digit = if negative { i + 1 } else { i };
162        if start_digit >= bytes.len() || !bytes[start_digit].is_ascii_digit() {
163            i += 1;
164            continue;
165        }
166        let start = i;
167        i = start_digit;
168        let mut dot = false;
169        while i < bytes.len() {
170            match bytes[i] {
171                b'0'..=b'9' => i += 1,
172                b'.' if !dot => {
173                    dot = true;
174                    i += 1;
175                }
176                _ => break,
177            }
178        }
179        let Ok(value) = s[start..i].parse::<f32>() else {
180            continue;
181        };
182        let mut j = i;
183        while j < bytes.len() && bytes[j].is_ascii_whitespace() {
184            j += 1;
185        }
186        let percent = j < bytes.len() && bytes[j] == b'%';
187        out.push((value, percent));
188    }
189    out
190}
191
192fn channel_byte(value: f32, percent: bool) -> u8 {
193    let v = if percent { value * 2.55 } else { value };
194    clampf(v, 0.0, 255.0).round() as u8
195}
196
197fn parse_color_text(s: &str) -> Option<(u8, u8, u8)> {
198    if let Some(hx) = parse_hex(s) {
199        return Some(hex_to_rgb(&hx));
200    }
201    let low = s.trim().to_ascii_lowercase();
202    let nums = color_components(&low);
203    if nums.len() < 3 {
204        return None;
205    }
206    if low.starts_with("rgb") {
207        return Some((
208            channel_byte(nums[0].0, nums[0].1),
209            channel_byte(nums[1].0, nums[1].1),
210            channel_byte(nums[2].0, nums[2].1),
211        ));
212    }
213    if low.starts_with("hsl") {
214        let h = nums[0].0;
215        let sat = clampf(nums[1].0, 0.0, 100.0) / 100.0;
216        let light = clampf(nums[2].0, 0.0, 100.0) / 100.0;
217        return Some(hsl_to_rgb(h, sat, light));
218    }
219    None
220}
221
222#[allow(dead_code)]
223fn fmt_val(v: f32, max: f32) -> String {
224    if max == 255.0 || max == 360.0 {
225        return format!("{}", v.round() as i32);
226    }
227    let r = v.round();
228    if (v - r).abs() < 0.05 {
229        format!("{r}")
230    } else {
231        format!("{:.1}", (v * 10.0).round() / 10.0)
232    }
233}
234
235struct Rec {
236    last_hex: String,
237    last_push: Option<Instant>,
238    in_history: bool,
239}
240
241#[derive(Clone, Copy)]
242struct PickerDrag {
243    origin: Point,
244    start: (f32, f32),
245}
246
247#[derive(Clone)]
248pub struct ColorPicker {
249    hue: Signal<f32>,
250    sat: Signal<f32>,
251    val: Signal<f32>,
252    model: Signal<usize>,
253    ref_hex: Signal<String>,
254    hex_text: Signal<String>,
255    editing_hex: Signal<bool>,
256    swatches: Signal<Vec<String>>,
257    undo: Signal<Vec<String>>,
258    redo: Signal<Vec<String>>,
259    active: Signal<Option<usize>>,
260    closed: Signal<bool>,
261    ch: [Signal<f32>; 3],
262    toasts: ToastQueue,
263    rec: Rc<RefCell<Rec>>,
264    on_close: Rc<RefCell<Option<Rc<dyn Fn()>>>>,
265
266    pos: Signal<(f32, f32)>,
267    drag: Rc<Cell<Option<PickerDrag>>>,
268    effects: Rc<RefCell<Option<EffectScope>>>,
269}
270
271pub type Picker = ColorPicker;
272
273impl Default for ColorPicker {
274    fn default() -> Self {
275        Self::new()
276    }
277}
278
279impl ColorPicker {
280    pub fn new() -> Self {
281        let (h, s, v) = (DEFAULT_H, DEFAULT_S, DEFAULT_V);
282        let (r, g, b) = hsv_to_rgb(h, s, v);
283        let start_hex = rgb_to_hex(r, g, b);
284        let p = Self {
285            hue: Signal::new(h),
286            sat: Signal::new(s),
287            val: Signal::new(v),
288            model: Signal::new(0),
289            ref_hex: Signal::new(start_hex.clone()),
290            hex_text: Signal::new(start_hex.clone()),
291            editing_hex: Signal::new(false),
292            swatches: Signal::new(DEFAULT_SWATCHES.iter().map(|s| s.to_string()).collect()),
293            undo: Signal::new(Vec::new()),
294            redo: Signal::new(Vec::new()),
295            active: Signal::new(None),
296            closed: Signal::new(false),
297            ch: [Signal::new(0.0), Signal::new(0.0), Signal::new(0.0)],
298            toasts: ToastQueue::new(),
299            rec: Rc::new(RefCell::new(Rec {
300                last_hex: start_hex,
301                last_push: None,
302                in_history: false,
303            })),
304            on_close: Rc::new(RefCell::new(None)),
305            pos: Signal::new((90.0, 70.0)),
306            drag: Rc::new(Cell::new(None)),
307            effects: Rc::new(RefCell::new(None)),
308        };
309        p.sync_channels();
310        p
311    }
312
313    pub fn set_hex(&self, hx: &str) {
314        self.apply_hex(hx);
315    }
316
317    pub fn hsv_signals(&self) -> (Signal<f32>, Signal<f32>, Signal<f32>) {
318        (self.hue.clone(), self.sat.clone(), self.val.clone())
319    }
320
321    pub fn toasts(&self) -> ToastQueue {
322        self.toasts.clone()
323    }
324
325    pub fn pos_signal(&self) -> Signal<(f32, f32)> {
326        self.pos.clone()
327    }
328
329    pub fn set_on_close(&self, f: impl Fn() + 'static) {
330        *self.on_close.borrow_mut() = Some(Rc::new(f));
331    }
332
333    fn close(&self) {
334        self.closed.set(true);
335        if let Some(f) = self.on_close.borrow().clone() {
336            f();
337        }
338    }
339
340    pub fn open(&self) {
341        self.closed.set(false);
342    }
343
344    pub fn is_open(&self) -> bool {
345        !self.closed.get_untracked()
346    }
347
348    pub fn cur_rgb(&self) -> (u8, u8, u8) {
349        hsv_to_rgb(
350            self.hue.get_untracked(),
351            self.sat.get_untracked(),
352            self.val.get_untracked(),
353        )
354    }
355
356    pub fn cur_hex(&self) -> String {
357        let (r, g, b) = self.cur_rgb();
358        rgb_to_hex(r, g, b)
359    }
360
361    pub fn cur_color(&self) -> Color {
362        let (r, g, b) = self.cur_rgb();
363        Color::rgb8(r, g, b)
364    }
365
366    fn set_rgb(&self, r: u8, g: u8, b: u8) {
367        let (nh, ns, nv) = rgb_to_hsv(r, g, b);
368        batch(|| {
369            if nv > 0.0001 {
370                if ns > 0.0001 {
371                    self.hue.set(nh);
372                }
373                self.sat.set(ns);
374            }
375            self.val.set(nv);
376        });
377    }
378
379    fn apply_hex(&self, hx: &str) {
380        if let Some(n) = parse_hex(hx) {
381            let (r, g, b) = hex_to_rgb(&n);
382            self.set_rgb(r, g, b);
383        }
384    }
385
386    fn channel_value(&self, idx: usize) -> f32 {
387        match self.model.get_untracked() {
388            0 => {
389                let (r, g, b) = self.cur_rgb();
390                match idx {
391                    0 => r as f32,
392                    1 => g as f32,
393                    _ => b as f32,
394                }
395            }
396            1 => match idx {
397                0 => self.hue.get_untracked(),
398                1 => self.sat.get_untracked() * 100.0,
399                _ => self.val.get_untracked() * 100.0,
400            },
401            _ => {
402                let (r, g, b) = self.cur_rgb();
403                let (h, s, l) = rgb_to_hsl(r, g, b);
404                let _ = h;
405                match idx {
406                    0 => self.hue.get_untracked(),
407                    1 => s * 100.0,
408                    _ => l * 100.0,
409                }
410            }
411        }
412    }
413
414    fn apply_channel(&self, idx: usize, v: f32) {
415        batch(|| match self.model.get_untracked() {
416            0 => {
417                let (r, g, b) = self.cur_rgb();
418                let q = clampf(v, 0.0, 255.0).round() as u8;
419                match idx {
420                    0 => self.set_rgb(q, g, b),
421                    1 => self.set_rgb(r, q, b),
422                    _ => self.set_rgb(r, g, q),
423                }
424            }
425            1 => match idx {
426                0 => self.hue.set(v.rem_euclid(360.0)),
427                1 => self.sat.set(clampf(v, 0.0, 100.0) / 100.0),
428                _ => self.val.set(clampf(v, 0.0, 100.0) / 100.0),
429            },
430            _ => {
431                if idx == 0 {
432                    self.hue.set(v.rem_euclid(360.0));
433                } else {
434                    let (r, g, b) = self.cur_rgb();
435                    let (hh, mut ss, mut ll) = rgb_to_hsl(r, g, b);
436                    if idx == 1 {
437                        ss = clampf(v, 0.0, 100.0) / 100.0;
438                    } else {
439                        ll = clampf(v, 0.0, 100.0) / 100.0;
440                    }
441                    let (nr, ng, nb) = hsl_to_rgb(hh, ss, ll);
442                    self.set_rgb(nr, ng, nb);
443                }
444            }
445        });
446    }
447
448    fn sync_channels(&self) {
449        for i in 0..3 {
450            let want = self.channel_value(i);
451            if (self.ch[i].get_untracked() - want).abs() > 1e-4 {
452                self.ch[i].set(want);
453            }
454        }
455        if !self.editing_hex.get_untracked() {
456            let hx = self.cur_hex();
457            if self.hex_text.get_untracked() != hx {
458                self.hex_text.set(hx);
459            }
460        }
461    }
462
463    fn note_change(&self, new_hex: String) {
464        let mut r = self.rec.borrow_mut();
465        if r.in_history || new_hex == r.last_hex {
466            return;
467        }
468        let now = Instant::now();
469        let coalesce = r
470            .last_push
471            .map(|t| now.duration_since(t) < HIST_WINDOW)
472            .unwrap_or(false);
473        if !coalesce {
474            let prev = r.last_hex.clone();
475            self.undo.update(|u| {
476                if u.last() != Some(&prev) {
477                    u.push(prev);
478                    if u.len() > HIST_LIMIT {
479                        u.remove(0);
480                    }
481                }
482            });
483            self.redo.set(Vec::new());
484        }
485        r.last_push = Some(now);
486        r.last_hex = new_hex;
487    }
488
489    pub fn do_undo(&self) {
490        let mut u = self.undo.get_untracked();
491        let Some(prev) = u.pop() else {
492            return;
493        };
494        let cur = self.cur_hex();
495        self.redo.update(|r| r.push(cur));
496        self.undo.set(u);
497        self.rec.borrow_mut().in_history = true;
498        self.apply_hex(&prev);
499        self.sync_channels();
500        let mut r = self.rec.borrow_mut();
501        r.last_hex = prev;
502        r.last_push = None;
503        r.in_history = false;
504    }
505
506    pub fn do_redo(&self) {
507        let mut rd = self.redo.get_untracked();
508        let Some(next) = rd.pop() else {
509            return;
510        };
511        let cur = self.cur_hex();
512        self.undo.update(|u| u.push(cur));
513        self.redo.set(rd);
514        self.rec.borrow_mut().in_history = true;
515        self.apply_hex(&next);
516        self.sync_channels();
517        let mut r = self.rec.borrow_mut();
518        r.last_hex = next;
519        r.last_push = None;
520        r.in_history = false;
521    }
522
523    pub fn set_reference(&self) {
524        self.ref_hex.set(self.cur_hex());
525        self.toast("Reference color set");
526    }
527
528    fn revert(&self) {
529        let rf = self.ref_hex.get_untracked();
530        if rf == self.cur_hex() {
531            return;
532        }
533        self.apply_hex(&rf);
534        self.toast("Reverted to reference");
535    }
536
537    fn with_clip(&self, env: &Environment, f: impl FnOnce(&SharedClipboard)) -> bool {
538        match env.get::<SharedClipboard>() {
539            Some(c) => {
540                f(&c);
541                true
542            }
543            None => false,
544        }
545    }
546
547    fn toast(&self, msg: impl Into<String>) {
548        self.toasts.info(msg, "");
549    }
550
551    fn copy_hex(&self, env: &Environment) {
552        let t = format!("#{}", self.cur_hex());
553        if self.with_clip(env, |c| c.0.set_text(&t)) {
554            self.toast(format!("Copied {t}"));
555        } else {
556            self.toast("Clipboard unavailable");
557        }
558    }
559
560    fn copy_rgb(&self, env: &Environment) {
561        let (r, g, b) = self.cur_rgb();
562        let t = format!("rgb({r}, {g}, {b})");
563        if self.with_clip(env, |c| c.0.set_text(&t)) {
564            self.toast(format!("Copied {t}"));
565        } else {
566            self.toast("Clipboard unavailable");
567        }
568    }
569
570    fn copy_hsl(&self, env: &Environment) {
571        let (r, g, b) = self.cur_rgb();
572        let (h, s, l) = rgb_to_hsl(r, g, b);
573        let t = format!(
574            "hsl({}, {}%, {}%)",
575            h.round() as i32,
576            (s * 100.0).round() as i32,
577            (l * 100.0).round() as i32
578        );
579        if self.with_clip(env, |c| c.0.set_text(&t)) {
580            self.toast(format!("Copied {t}"));
581        } else {
582            self.toast("Clipboard unavailable");
583        }
584    }
585
586    fn paste(&self, env: &Environment) {
587        let Some(clip) = env.get::<SharedClipboard>() else {
588            self.toast("Clipboard access denied");
589            return;
590        };
591        let Some(txt) = clip.0.get_text() else {
592            self.toast("Clipboard access denied");
593            return;
594        };
595        let txt = txt.trim().to_string();
596        if let Some((r, g, b)) = parse_color_text(&txt) {
597            self.set_rgb(r, g, b);
598            self.toast(format!("Pasted #{}", self.cur_hex()));
599            return;
600        }
601        self.toast("No color found in clipboard");
602    }
603
604    fn sw_add(&self) {
605        let hx = self.cur_hex();
606        let mut sw = self.swatches.get_untracked();
607        if sw.contains(&hx) {
608            self.toast(format!("#{hx} is already in swatches"));
609            return;
610        }
611        sw.push(hx);
612        self.swatches.set(sw);
613    }
614
615    fn sw_sort(&self) {
616        let mut sw = self.swatches.get_untracked();
617        sw.sort_by(|a, b| {
618            let (r1, g1, b1) = hex_to_rgb(a);
619            let (r2, g2, b2) = hex_to_rgb(b);
620            let (h1, s1, v1) = rgb_to_hsv(r1, g1, b1);
621            let (h2, s2, v2) = rgb_to_hsv(r2, g2, b2);
622            let _ = (s1, v1, s2, v2);
623            let g1 = if s1 < 0.06 { 999.0 } else { h1 };
624            let g2 = if s2 < 0.06 { 999.0 } else { h2 };
625            g1.partial_cmp(&g2)
626                .unwrap_or(std::cmp::Ordering::Equal)
627                .then(v2.partial_cmp(&v1).unwrap_or(std::cmp::Ordering::Equal))
628        });
629        self.swatches.set(sw);
630    }
631
632    fn effect_handle(&self) -> Self {
633        let mut handle = self.clone();
634        handle.effects = Rc::new(RefCell::new(None));
635        handle
636    }
637
638    pub fn install_effects(&self) {
639        if self.effects.borrow().is_some() {
640            return;
641        }
642        self.rec.borrow_mut().last_hex = self.cur_hex();
643
644        let p = self.effect_handle();
645        let s1 = effect(move || {
646            let _ = (p.hue.get(), p.sat.get(), p.val.get(), p.model.get());
647            p.note_change(p.cur_hex());
648            p.sync_channels();
649        });
650
651        let p = self.effect_handle();
652        let s2 = effect(move || {
653            let cur = [p.ch[0].get(), p.ch[1].get(), p.ch[2].get()];
654            let want = [p.channel_value(0), p.channel_value(1), p.channel_value(2)];
655            let eps = if p.model.get_untracked() == 0 {
656                0.51
657            } else {
658                0.02
659            };
660            let mut hit = None;
661            for i in 0..3 {
662                if (cur[i] - want[i]).abs() > eps {
663                    hit = Some(i);
664                    break;
665                }
666            }
667            if let Some(i) = hit {
668                p.active.set(Some(i));
669                p.apply_channel(i, cur[i]);
670            }
671        });
672
673        let p = self.effect_handle();
674        let s3 = effect(move || {
675            let t = p.hex_text.get();
676            if t.len() == 6
677                && let Some(n) = parse_hex(&t)
678                && n != p.cur_hex()
679            {
680                p.apply_hex(&n);
681            }
682        });
683
684        let mut scope = EffectScope::new();
685        scope.extend([s1, s2, s3]);
686        *self.effects.borrow_mut() = Some(scope);
687    }
688
689    pub fn uninstall_effects(&self) {
690        if let Some(mut scope) = self.effects.borrow_mut().take() {
691            scope.dispose_all();
692        }
693    }
694}
695
696fn channel_meta(model: usize, idx: usize) -> (&'static str, f32, f32, usize) {
697    match model {
698        0 => match idx {
699            0 => ("R", 0.0, 255.0, 0),
700            1 => ("G", 0.0, 255.0, 0),
701            _ => ("B", 0.0, 255.0, 0),
702        },
703        1 => match idx {
704            0 => ("H", 0.0, 360.0, 0),
705            1 => ("S", 0.0, 100.0, 1),
706            _ => ("V", 0.0, 100.0, 1),
707        },
708        _ => match idx {
709            0 => ("H", 0.0, 360.0, 0),
710            1 => ("S", 0.0, 100.0, 1),
711            _ => ("L", 0.0, 100.0, 1),
712        },
713    }
714}
715
716fn hue_stops() -> Vec<BrushStop> {
717    (0..=6)
718        .map(|i| {
719            BrushStop::new(
720                i as f32 / 6.0,
721                Color::from_hsva(i as f32 / 6.0, 1.0, 1.0, 1.0),
722            )
723        })
724        .collect()
725}
726
727fn channel_stops(p: &ColorPicker, idx: usize) -> Vec<BrushStop> {
728    let h = p.hue.get_untracked();
729    let s = p.sat.get_untracked();
730    let v = p.val.get_untracked();
731    let (r, g, b) = hsv_to_rgb(h, s, v);
732    match p.model.get_untracked() {
733        0 => {
734            let (a, c) = match idx {
735                0 => (Color::rgb8(0, g, b), Color::rgb8(255, g, b)),
736                1 => (Color::rgb8(r, 0, b), Color::rgb8(r, 255, b)),
737                _ => (Color::rgb8(r, g, 0), Color::rgb8(r, g, 255)),
738            };
739            vec![BrushStop::new(0.0, a), BrushStop::new(1.0, c)]
740        }
741        1 => match idx {
742            0 => hue_stops(),
743            1 => vec![
744                BrushStop::new(0.0, Color::from_hsva(h / 360.0, 0.0, v, 1.0)),
745                BrushStop::new(1.0, Color::from_hsva(h / 360.0, 1.0, v, 1.0)),
746            ],
747            _ => vec![
748                BrushStop::new(0.0, Color::BLACK),
749                BrushStop::new(1.0, Color::from_hsva(h / 360.0, s, 1.0, 1.0)),
750            ],
751        },
752        _ => {
753            let (_, _, l) = rgb_to_hsl(r, g, b);
754            match idx {
755                0 => hue_stops(),
756                1 => vec![
757                    BrushStop::new(0.0, Color::from_hsl(h, 0.0, l)),
758                    BrushStop::new(1.0, Color::from_hsl(h, 1.0, l)),
759                ],
760                _ => vec![
761                    BrushStop::new(0.0, Color::BLACK),
762                    BrushStop::new(0.5, Color::from_hsl(h, 1.0, 0.5)),
763                    BrushStop::new(1.0, Color::WHITE),
764                ],
765            }
766        }
767    }
768}
769
770#[derive(Clone)]
771struct ChanRow {
772    p: ColorPicker,
773    idx: usize,
774}
775
776impl View for ChanRow {
777    fn body(&self) -> impl View + 'static {
778        let m = self.p.model.get();
779        let _ = (self.p.hue.get(), self.p.sat.get(), self.p.val.get());
780        let act = self.p.active.get();
781        let (label, min, max, prec) = channel_meta(m, self.idx);
782        let stops = channel_stops(&self.p, self.idx);
783        let sig = self.p.ch[self.idx].clone();
784        let sig2 = sig.clone();
785        let anchored = act == Some(self.idx);
786
787        let pc = self.p.clone();
788        let idx = self.idx;
789        let track = Slider1D::new(sig, min, max)
790            .gradient(stops, Orientation::Horizontal)
791            .track_height(8.0)
792            .on_commit(move |val| {
793                pc.active.set(Some(idx));
794                pc.apply_channel(idx, val);
795            });
796
797        let pc = self.p.clone();
798        let idx = self.idx;
799        let mut value = DragValue::new(sig2)
800            .min(min)
801            .max(max)
802            .precision(prec)
803            .width(52.0)
804            .line_full(true)
805            .on_commit(move |val| {
806                pc.active.set(Some(idx));
807                pc.apply_channel(idx, val);
808            });
809        if anchored {
810            value = value.line_color(ACCENT_ACTIVE).line_width(2.0);
811        }
812
813        Row::new()
814            .gap(8.0)
815            .align_items(Align::Center)
816            .child(
817                Text::new(label)
818                    .size(11.0)
819                    .color(if anchored {
820                        TEXT_PRIMARY
821                    } else {
822                        TEXT_SECONDARY
823                    })
824                    .width(11.0),
825            )
826            .child_flex(track, 1.0)
827            .child(value)
828    }
829}
830
831#[derive(Clone)]
832struct PreviewBox {
833    p: ColorPicker,
834}
835
836impl PreviewBox {
837    const W: f32 = 64.0;
838    const H: f32 = 24.0;
839
840    const SPLIT: f32 = Self::W * 1.6 / 2.6;
841}
842
843impl View for PreviewBox {
844    fn body(&self) -> impl View + 'static {
845        let pd = self.p.clone();
846        let pc = self.p.clone();
847        let pt = self.p.clone();
848        let armed = Rc::new(Cell::new(false));
849        let armed_up = armed.clone();
850        Container::new()
851            .width(Self::W)
852            .height(Self::H)
853            .border(1.0, HAIR_TRACK)
854            .radius(2.0)
855            .clip(true)
856            .child(
857                Canvas::new()
858                    .cursor(CursorIcon::Pointer)
859                    .on_draw(move |g| {
860                        let new_c = pd.cur_color();
861                        let ref_c = Color::parse_hex(&pd.ref_hex.get_untracked()).unwrap_or(new_c);
862                        g.fill(
863                            Rect::from_xywh(0.0, 0.0, Self::SPLIT + 1.0, Self::H),
864                            Fill::solid(new_c),
865                        );
866                        g.fill(
867                            Rect::from_xywh(Self::SPLIT, 0.0, Self::W - Self::SPLIT, Self::H),
868                            Fill::solid(ref_c),
869                        );
870                    })
871                    .on_pointer(
872                        move |pos: Point, ev: &PointerEvent, ctx: &mut EventCtx| match ev {
873                            PointerEvent::Down {
874                                button: PointerButton::Left,
875                                ..
876                            } => {
877                                armed.set(pos.x >= Self::SPLIT);
878                                if armed.get() {
879                                    ctx.capture_pointer();
880                                    ctx.handled();
881                                }
882                            }
883                            PointerEvent::Up {
884                                button: PointerButton::Left,
885                                ..
886                            } => {
887                                if armed_up.take() && pos.x >= Self::SPLIT {
888                                    pc.revert();
889                                }
890                                ctx.handled();
891                            }
892                            _ => {}
893                        },
894                    )
895                    .track(move || {
896                        pt.hue.get();
897                        pt.sat.get();
898                        pt.val.get();
899                        pt.ref_hex.get();
900                    }),
901            )
902    }
903}
904
905#[derive(Clone)]
906struct SwatchBox {
907    p: ColorPicker,
908    hx: String,
909}
910
911impl View for SwatchBox {
912    fn body(&self) -> impl View + 'static {
913        let _ = (self.p.hue.get(), self.p.sat.get(), self.p.val.get());
914        let selected = self.p.cur_hex() == self.hx;
915        let bg = Color::parse_hex(&self.hx).unwrap_or(Color::BLACK);
916        let pa = self.p.clone();
917        let hx = self.hx.clone();
918        let cell = Container::new()
919            .width(22.0)
920            .height(22.0)
921            .background(bg)
922            .border(
923                1.0,
924                if selected {
925                    Color::rgb8(0x35, 0xC2, 0xF1)
926                } else {
927                    HAIR_TRACK
928                },
929            )
930            .radius(2.0)
931            .on_click(move || pa.apply_hex(&hx));
932        let po = self.p.clone();
933        let hx_a = self.hx.clone();
934        let hx2 = self.hx.clone();
935        let hx3 = self.hx.clone();
936        let hx_d = self.hx.clone();
937        ContextMenu::new(cell)
938            .item("Zastosuj", move || po.apply_hex(&hx_a))
939            .item("Overwrite with current", {
940                let po = self.p.clone();
941                move || {
942                    let cur = po.cur_hex();
943                    po.swatches.update(|sw| {
944                        if let Some(slot) = sw.iter_mut().find(|s| *s == &hx2) {
945                            *slot = cur;
946                        }
947                    });
948                }
949            })
950            .item_env("Kopiuj HEX", {
951                let po = self.p.clone();
952                move |env: &Environment| {
953                    let t = format!("#{hx3}");
954                    match env.get::<SharedClipboard>() {
955                        Some(c) => {
956                            c.0.set_text(&t);
957                            po.toast(format!("Copied {t}"));
958                        }
959                        None => po.toast("Clipboard unavailable"),
960                    }
961                }
962            })
963            .item("Delete", {
964                let po = self.p.clone();
965                move || {
966                    po.swatches.update(|sw| sw.retain(|s| *s != hx_d));
967                }
968            })
969    }
970}
971
972#[derive(Clone)]
973struct SwatchesView {
974    p: ColorPicker,
975}
976
977impl View for SwatchesView {
978    fn body(&self) -> impl View + 'static {
979        let sw = self.p.swatches.get();
980        let _ = (self.p.hue.get(), self.p.sat.get(), self.p.val.get());
981        let mut wrap = Wrap::new()
982            .gap(6.0)
983            .padding_xy(10.0, 0.0)
984            .padding_bottom(10.0);
985        let pa = self.p.clone();
986        wrap = wrap.child(
987            Button::icon("plus")
988                .icon_size(12.0)
989                .padding(5.0, 5.0)
990                .on_click(move || pa.sw_add()),
991        );
992        for hx in sw {
993            wrap = wrap.child(SwatchBox {
994                p: self.p.clone(),
995                hx,
996            });
997        }
998        wrap
999    }
1000}
1001
1002impl ColorPicker {
1003    pub fn panel(&self) -> impl View + 'static {
1004        self.install_effects();
1005        let p = self.clone();
1006        let title = Text::with(move || {
1007            let dirty = p.cur_hex() != p.ref_hex.get();
1008            format!("BASE COLOR{}", if dirty { " *" } else { "" })
1009        })
1010        .size(11.0)
1011        .bold()
1012        .color(TEXT_TITLE);
1013
1014        let pc = self.clone();
1015        let header = Container::new()
1016            .fill_width()
1017            .height(34.0)
1018            .background(BG_HEADER)
1019            .border_bottom(1.0, BORDER_PANEL)
1020            .padding_xy(10.0, 0.0)
1021            .child(
1022                Row::new()
1023                    .fill_size()
1024                    .gap(8.0)
1025                    .align_items(Align::Center)
1026                    .child_flex(title, 1.0)
1027                    .child(
1028                        Button::icon("close")
1029                            .icon_size(12.0)
1030                            .padding_xy(8.0, 7.0)
1031                            .on_click(move || pc.close()),
1032                    ),
1033            );
1034
1035        let picker_row = Row::new()
1036            .gap(8.0)
1037            .padding_xy(10.0, 10.0)
1038            .child_flex(
1039                SvPad::new(self.sat.clone(), self.val.clone(), self.hue.clone()),
1040                1.0,
1041            )
1042            .child(
1043                Slider1D::vertical(self.hue.clone(), 0.0, 360.0)
1044                    .gradient(hue_stops(), Orientation::Vertical)
1045                    .track_height(8.0)
1046                    .width(14.0),
1047            );
1048
1049        let pp = self.clone();
1050        let hex_field = TextField::new(self.hex_text.clone())
1051            .max_length(6)
1052            .width(88.0)
1053            .height(26.0)
1054            .font_size(12.0)
1055            .padding_xy(7.0, 0.0)
1056            .background(BG_INPUT)
1057            .border(1.0, BORDER_CONTROL)
1058            .radius(3.0)
1059            .on_edit_start({
1060                let pe = self.clone();
1061                move || pe.editing_hex.set(true)
1062            })
1063            .on_edit_end({
1064                let pe = self.clone();
1065                move |t: String| {
1066                    pe.editing_hex.set(false);
1067                    if parse_hex(&t).is_some() {
1068                        pe.apply_hex(&t);
1069                    } else {
1070                        pe.toast("Invalid hex");
1071                    }
1072                    pe.sync_channels();
1073                }
1074            })
1075            .on_submit({
1076                let pe = self.clone();
1077                move |t: String| {
1078                    pe.editing_hex.set(false);
1079                    if parse_hex(&t).is_some() {
1080                        pe.apply_hex(&t);
1081                    }
1082                    pe.sync_channels();
1083                }
1084            });
1085
1086        let info_row = Row::new()
1087            .gap(8.0)
1088            .padding_x(10.0)
1089            .align_items(Align::Center)
1090            .child(PreviewBox { p: self.clone() })
1091            .child(Spacer::flex(1.0))
1092            .child(Text::new("#").size(11.0).color(TEXT_SECONDARY))
1093            .child(hex_field)
1094            .child(Button::icon("pipette").icon_size(16.0).on_click(move || {
1095                pp.toast("Eyedropper unavailable in this build (browser-only API)");
1096            }));
1097
1098        let pm = self.clone();
1099        let model_drop = DropdownButton::new(
1100            self.model.clone(),
1101            vec!["RGB".to_string(), "HSV".to_string(), "HSL".to_string()],
1102        )
1103        .font_size(12.0)
1104        .padding_xy(7.0, 3.0)
1105        .on_select(move |i| {
1106            pm.model.set(i);
1107            pm.active.set(None);
1108            pm.sync_channels();
1109        });
1110        let model_row = Row::new()
1111            .height(26.0)
1112            .padding_x(10.0)
1113            .align_items(Align::Center)
1114            .child(model_drop)
1115            .child(Spacer::flex(1.0))
1116            .child(self.color_menu());
1117
1118        let channels = Column::new()
1119            .gap(4.0)
1120            .padding_x(10.0)
1121            .child(ChanRow {
1122                p: self.clone(),
1123                idx: 0,
1124            })
1125            .child(ChanRow {
1126                p: self.clone(),
1127                idx: 1,
1128            })
1129            .child(ChanRow {
1130                p: self.clone(),
1131                idx: 2,
1132            });
1133
1134        let section = Container::new()
1135            .fill_width()
1136            .height(24.0)
1137            .background(BG_SECTION)
1138            .border_top(1.0, BORDER_HAIRLINE)
1139            .border_bottom(1.0, BORDER_HAIRLINE)
1140            .padding_xy(10.0, 0.0)
1141            .child(
1142                Row::new()
1143                    .fill_size()
1144                    .align_items(Align::Center)
1145                    .child(Text::new("Swatches").size(11.0).color(TEXT_PRIMARY))
1146                    .child(Spacer::flex(1.0))
1147                    .child(self.sw_menu()),
1148            );
1149
1150        let body = Column::new()
1151            .fill_width()
1152            .gap(10.0)
1153            .child(picker_row)
1154            .child(info_row)
1155            .child(model_row)
1156            .child(channels)
1157            .child(section)
1158            .child(SwatchesView { p: self.clone() });
1159
1160        Container::new()
1161            .width(280.0)
1162            .background(BG_PANEL)
1163            .border(1.0, BORDER_PANEL)
1164            .child(Column::new().fill_width().child(header).child(body))
1165    }
1166
1167    fn color_menu(&self) -> impl View + 'static {
1168        let dots = Button::new("···").font_size(11.0).padding_xy(6.0, 2.0);
1169        let p = self.clone();
1170        FloatingMenu::wrap(dots)
1171            .below_invoker()
1172            .open_on_click()
1173            .text_row("Cofnij  Ctrl+Z", {
1174                let p = p.clone();
1175                move || p.do_undo()
1176            })
1177            .text_row("Redo Ctrl+Y", {
1178                let p = p.clone();
1179                move || p.do_redo()
1180            })
1181            .separator()
1182            .text_row_env("Kopiuj HEX", {
1183                let p = p.clone();
1184                move |env: &Environment| p.copy_hex(env)
1185            })
1186            .text_row_env("Kopiuj RGB", {
1187                let p = p.clone();
1188                move |env: &Environment| p.copy_rgb(env)
1189            })
1190            .text_row_env("Kopiuj HSL", {
1191                let p = p.clone();
1192                move |env: &Environment| p.copy_hsl(env)
1193            })
1194            .separator()
1195            .text_row_env("Wklej  Ctrl+Shift+V", {
1196                let p = p.clone();
1197                move |env: &Environment| p.paste(env)
1198            })
1199            .text_row("Pipeta  E", {
1200                let p = p.clone();
1201                move || p.toast("Eyedropper unavailable in this build (browser-only API)")
1202            })
1203            .separator()
1204            .text_row("Ustaw jako odniesienie  Ctrl+S", {
1205                let p = p.clone();
1206                move || p.set_reference()
1207            })
1208    }
1209
1210    fn sw_menu(&self) -> impl View + 'static {
1211        let dots = Button::new("···").font_size(11.0).padding_xy(6.0, 2.0);
1212        let p = self.clone();
1213        FloatingMenu::wrap(dots)
1214            .below_invoker()
1215            .open_on_click()
1216            .text_row("Add Current Color", {
1217                let p = p.clone();
1218                move || p.sw_add()
1219            })
1220            .text_row("Sortuj wg odcienia", {
1221                let p = p.clone();
1222                move || p.sw_sort()
1223            })
1224            .separator()
1225            .text_row("Restore Defaults", {
1226                let p = p.clone();
1227                move || {
1228                    p.swatches
1229                        .set(DEFAULT_SWATCHES.iter().map(|s| s.to_string()).collect());
1230                    p.toast("Default swatches restored");
1231                }
1232            })
1233            .text_row("Clear", {
1234                let p = p.clone();
1235                move || {
1236                    p.swatches.set(Vec::new());
1237                    p.toast("Swatches cleared");
1238                }
1239            })
1240    }
1241
1242    pub fn floating(&self) -> PickerFloat {
1243        PickerFloat {
1244            picker: self.clone(),
1245        }
1246    }
1247}
1248
1249const PICKER_HEADER_H: f32 = 34.0;
1250const PICKER_CLOSE_W: f32 = 40.0;
1251
1252#[derive(Clone)]
1253pub struct PickerFloat {
1254    picker: ColorPicker,
1255}
1256
1257impl PickerFloat {
1258    fn picker_style(&self) -> LayoutStyle {
1259        let (x, y) = self.picker.pos.get_untracked();
1260        LayoutStyle {
1261            width: Length::Fit,
1262            height: Length::Fit,
1263            position: Position::Absolute(Inset::xy(x, y)),
1264            ..LayoutStyle::default()
1265        }
1266    }
1267}
1268
1269impl View for PickerFloat {
1270    fn body(&self) -> impl View + 'static {
1271        let _ = self.picker.pos.get();
1272        let _ = self.picker.closed.get();
1273        Unit
1274    }
1275
1276    fn children_override(&self) -> Option<Vec<AnyView>> {
1277        if self.picker.closed.get() {
1278            return Some(Vec::new());
1279        }
1280        Some(vec![AnyView::new(self.picker.panel())])
1281    }
1282
1283    fn layout_style(&self) -> LayoutStyle {
1284        self.picker_style()
1285    }
1286
1287    fn layout_style_ctx(
1288        &self,
1289        _rt: &sequoia_view::Runtime,
1290        _el: sequoia_view::ElementId,
1291    ) -> LayoutStyle {
1292        self.picker_style()
1293    }
1294
1295    fn accessibility(&self) -> Option<sequoia_view::Accessible> {
1296        Some(sequoia_view::Accessible::new(
1297            sequoia_view::AccessibleRole::Group,
1298            "Color picker",
1299        ))
1300    }
1301
1302    fn has_visual_state(&self) -> bool {
1303        true
1304    }
1305
1306    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
1307        if self.picker.drag.get().is_some() {
1308            CursorIcon::Grabbing
1309        } else {
1310            CursorIcon::Default
1311        }
1312    }
1313
1314    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
1315        if !self.picker.is_open() {
1316            return;
1317        }
1318        match event {
1319            Event::Pointer(PointerEvent::Down {
1320                button: PointerButton::Left,
1321                pos,
1322                ..
1323            }) => {
1324                let head = Rect::from_xywh(
1325                    ctx.rect.left(),
1326                    ctx.rect.top(),
1327                    (ctx.rect.size.width - PICKER_CLOSE_W).max(0.0),
1328                    PICKER_HEADER_H.min(ctx.rect.size.height),
1329                );
1330                if head.contains(*pos) {
1331                    let (cx, cy) = self.picker.pos.get_untracked();
1332                    self.picker.drag.set(Some(PickerDrag {
1333                        origin: *pos,
1334                        start: (cx, cy),
1335                    }));
1336                    ctx.capture_pointer();
1337                    ctx.handled();
1338                }
1339            }
1340            Event::Pointer(PointerEvent::Move { pos, .. }) => {
1341                if let Some(st) = self.picker.drag.get() {
1342                    let nx = st.start.0 + (pos.x - st.origin.x);
1343                    let ny = st.start.1 + (pos.y - st.origin.y);
1344                    if nx.is_finite() && ny.is_finite() {
1345                        let win = ctx.window_size();
1346                        let max_x = (win.width - 120.0).max(0.0);
1347                        let max_y = (win.height - PICKER_HEADER_H).max(0.0);
1348                        self.picker
1349                            .pos
1350                            .set((nx.clamp(0.0, max_x), ny.clamp(0.0, max_y)));
1351                    }
1352                    ctx.handled();
1353                }
1354            }
1355            Event::Pointer(PointerEvent::Up {
1356                button: PointerButton::Left,
1357                ..
1358            }) => {
1359                if self.picker.drag.take().is_some() {
1360                    ctx.handled();
1361                }
1362            }
1363            Event::Pointer(PointerEvent::Cancel) => {
1364                self.picker.drag.set(None);
1365            }
1366            _ => {}
1367        }
1368    }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374
1375    #[test]
1376    fn hex_parser_is_strict_and_accepts_short_forms() {
1377        assert_eq!(parse_hex("#abc"), Some("AABBCC".to_string()));
1378        assert_eq!(parse_hex("A1b2C3"), Some("A1B2C3".to_string()));
1379        assert_eq!(parse_hex("0x112233"), Some("112233".to_string()));
1380        assert_eq!(parse_hex("11223344"), Some("112233".to_string()));
1381        assert_eq!(parse_hex("abcd"), Some("AABBCC".to_string()));
1382        assert_eq!(parse_hex("rgb(255, 0, 0)"), None);
1383        assert_eq!(parse_hex("GGGGGG"), None);
1384        assert_eq!(parse_hex("#12345"), None);
1385        assert_eq!(parse_hex(""), None);
1386    }
1387
1388    #[test]
1389    fn color_text_parser_handles_css_rgb_and_hsl() {
1390        assert_eq!(parse_color_text("#f00"), Some((255, 0, 0)));
1391        assert_eq!(parse_color_text("rgb(255, 0, 0)"), Some((255, 0, 0)));
1392        assert_eq!(parse_color_text("rgb(100%, 0%, 50%)"), Some((255, 0, 128)));
1393        assert_eq!(
1394            parse_color_text("rgba(10, 20, 30, 0.5)"),
1395            Some((10, 20, 30))
1396        );
1397        assert_eq!(parse_color_text("hsl(120, 100%, 50%)"), Some((0, 255, 0)));
1398        assert_eq!(parse_color_text("nope"), None);
1399    }
1400
1401    #[test]
1402    fn set_hex_updates_picker_and_channel_text() {
1403        let picker = ColorPicker::new();
1404        picker.install_effects();
1405        picker.install_effects();
1406        picker.set_hex("#FF0000");
1407        assert_eq!(picker.cur_hex(), "FF0000");
1408        assert_eq!(picker.hex_text.get_untracked(), "FF0000");
1409        picker.set_hex("#00ff00");
1410        assert_eq!(picker.cur_hex(), "00FF00");
1411    }
1412
1413    #[test]
1414    fn close_and_open_toggle_floating_state() {
1415        let picker = ColorPicker::new();
1416        assert!(picker.is_open());
1417        picker.close();
1418        assert!(!picker.is_open());
1419        picker.open();
1420        assert!(picker.is_open());
1421    }
1422
1423    #[test]
1424    fn floating_uses_the_position_signal() {
1425        let picker = ColorPicker::new();
1426        picker.pos_signal().set((123.0, 45.0));
1427        let style = picker.floating().picker_style();
1428        match style.position {
1429            Position::Absolute(inset) => {
1430                assert_eq!(inset.left, Some(123.0));
1431                assert_eq!(inset.top, Some(45.0));
1432            }
1433            _ => panic!("expected absolute position"),
1434        }
1435    }
1436}