Skip to main content

agg_gui/widgets/
color_picker.rs

1//! `ColorPicker` — an inline-expanding colour selection widget.
2//!
3//! Click the swatch to open a panel with a hue slider, a saturation/value
4//! rectangle, an alpha slider, a hex readout, an optional "No Color (Pass
5//! Through)" checkbox, and Cancel / Select buttons.  Bound to an
6//! `Rc<Cell<Color>>` so callers observe changes through the standard shared
7//! state pattern.
8//!
9//! Layout mirrors `ComboBox`: when closed the widget reports a compact height;
10//! when open it returns the full expanded height so sibling widgets are pushed
11//! down (works naturally inside a `ScrollView` or a `Window::with_auto_size`).
12//!
13//! # Composition
14//!
15//! ```text
16//! ColorPicker (swatch + custom gradients)
17//!   ├── Checkbox   (No Color)
18//!   ├── Button     (Cancel)
19//!   └── Button     (Select)
20//! ```
21//!
22//! Gradients (hue/SV/alpha) are painted directly as stacks of thin coloured
23//! slices — agg-gui has no gradient primitive, but 1-px slices at this scale
24//! are cheap and banding-free.
25
26use std::cell::Cell;
27use std::rc::Rc;
28use std::sync::Arc;
29
30use crate::color::Color;
31use crate::draw_ctx::DrawCtx;
32use crate::event::{Event, EventResult, MouseButton};
33use crate::geometry::{Point, Rect, Size};
34use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
35use crate::text::Font;
36use crate::widget::{Widget, paint_subtree};
37use crate::widgets::button::Button;
38use crate::widgets::checkbox::Checkbox;
39
40// ── Layout constants ─────────────────────────────────────────────────────────
41
42const SWATCH_H:    f64 = 22.0;
43const SWATCH_MIN_W:f64 = 48.0;
44
45const PANEL_W:     f64 = 228.0;
46const PAD:         f64 = 8.0;
47const ROW_GAP:     f64 = 6.0;
48
49const HUE_H:       f64 = 16.0;
50const SV_H:        f64 = 140.0;
51const ALPHA_H:     f64 = 16.0;
52const HEX_H:       f64 = 20.0;
53const CHECK_H:     f64 = 20.0;
54const BTN_H:       f64 = 26.0;
55
56/// Height of the expanded panel below the swatch (does NOT include the swatch).
57fn panel_body_h(allow_none: bool) -> f64 {
58    let mut h = PAD;
59    h += HUE_H + ROW_GAP;
60    h += SV_H  + ROW_GAP;
61    h += ALPHA_H + ROW_GAP;
62    h += HEX_H + ROW_GAP;
63    if allow_none { h += CHECK_H + ROW_GAP; }
64    h += BTN_H + PAD;
65    h
66}
67
68// ── HSV / RGB helpers ────────────────────────────────────────────────────────
69
70fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
71    let max = r.max(g).max(b);
72    let min = r.min(g).min(b);
73    let d = max - min;
74    let v = max;
75    let s = if max <= 0.0 { 0.0 } else { d / max };
76    let h = if d <= 0.0 {
77        0.0
78    } else if max == r {
79        60.0 * (((g - b) / d) % 6.0)
80    } else if max == g {
81        60.0 * (((b - r) / d) + 2.0)
82    } else {
83        60.0 * (((r - g) / d) + 4.0)
84    };
85    let h = if h < 0.0 { h + 360.0 } else { h };
86    (h / 360.0, s, v)
87}
88
89fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
90    let h6 = (h * 6.0) % 6.0;
91    let c  = v * s;
92    let x  = c * (1.0 - (h6 % 2.0 - 1.0).abs());
93    let (r1, g1, b1) = match h6 as i32 {
94        0 => (c, x, 0.0),
95        1 => (x, c, 0.0),
96        2 => (0.0, c, x),
97        3 => (0.0, x, c),
98        4 => (x, 0.0, c),
99        _ => (c, 0.0, x),
100    };
101    let m = v - c;
102    (r1 + m, g1 + m, b1 + m)
103}
104
105fn format_hex(c: Color) -> String {
106    let r = (c.r * 255.0).clamp(0.0, 255.0) as u32;
107    let g = (c.g * 255.0).clamp(0.0, 255.0) as u32;
108    let b = (c.b * 255.0).clamp(0.0, 255.0) as u32;
109    let a = (c.a * 255.0).clamp(0.0, 255.0) as u32;
110    format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
111}
112
113// ── Drag mode ────────────────────────────────────────────────────────────────
114
115#[derive(Clone, Copy, Debug, PartialEq)]
116enum Drag { None, Hue, Sv, Alpha }
117
118// ── Widget ───────────────────────────────────────────────────────────────────
119
120/// Inline colour picker bound to a shared `Color` cell.
121pub struct ColorPicker {
122    bounds:   Rect,
123    children: Vec<Box<dyn Widget>>, // [no_color_check?, cancel, select]
124    base:     WidgetBase,
125
126    font:      Arc<Font>,
127    font_size: f64,
128
129    /// Authoritative colour the caller observes.  Only written on Select (or
130    /// when "No Color" toggles, depending on wiring).
131    color_cell: Rc<Cell<Color>>,
132
133    /// Snapshot taken when the picker was opened — restored on Cancel.
134    saved: Color,
135
136    /// Working state while the panel is open.
137    open:     bool,
138    h: f32, s: f32, v: f32, a: f32,
139    /// True when "No Color (Pass Through)" is checked — working state; applied
140    /// to the cell on Select as `Color::transparent()`.
141    no_color: bool,
142    allow_none: bool,
143
144    /// None means not currently dragging anything.
145    drag: Drag,
146
147    /// Last local mouse position — fed into child widget layout for hit tests.
148    hovered: bool,
149
150    /// Optional callback invoked on Select with the final colour.
151    on_select: Option<Box<dyn FnMut(Color)>>,
152
153    // ── Sub-widget indices into `children` ───────────────────────────────────
154    /// Set during `build_children` so paint/layout can find them quickly.
155    idx_cancel: usize,
156    idx_select: usize,
157    idx_none:   Option<usize>,
158
159    /// Shared "no color" checkbox state.  Owned by `ColorPicker` so `on_event`
160    /// can react to changes without going through a callback chain.
161    none_cell: Rc<Cell<bool>>,
162    /// Shared flags the sub-buttons flip; read + cleared by `on_event`.
163    cancel_flag: Rc<Cell<bool>>,
164    select_flag: Rc<Cell<bool>>,
165}
166
167impl ColorPicker {
168    pub fn new(color_cell: Rc<Cell<Color>>, font: Arc<Font>) -> Self {
169        let initial = color_cell.get();
170        let (h, s, v) = rgb_to_hsv(initial.r, initial.g, initial.b);
171        let none_cell   = Rc::new(Cell::new(false));
172        let cancel_flag = Rc::new(Cell::new(false));
173        let select_flag = Rc::new(Cell::new(false));
174
175        let mut me = Self {
176            bounds:   Rect::default(),
177            children: Vec::new(),
178            base:     WidgetBase::new(),
179            font:     Arc::clone(&font),
180            font_size: 13.0,
181            color_cell,
182            saved:    initial,
183            open:     false,
184            h, s, v,
185            a: initial.a,
186            no_color: initial.a <= 0.0,
187            allow_none: false,
188            drag: Drag::None,
189            hovered: false,
190            on_select: None,
191            idx_cancel: 0,
192            idx_select: 1,
193            idx_none:   None,
194            none_cell,
195            cancel_flag,
196            select_flag,
197        };
198        me.build_children();
199        me
200    }
201
202    pub fn with_font_size(mut self, s: f64) -> Self { self.font_size = s; self }
203    pub fn with_allow_none(mut self, allow: bool) -> Self {
204        self.allow_none = allow;
205        self.build_children();
206        self
207    }
208    pub fn on_select(mut self, cb: impl FnMut(Color) + 'static) -> Self {
209        self.on_select = Some(Box::new(cb));
210        self
211    }
212
213    pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
214    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
215    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
216    pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
217    pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
218
219    fn build_children(&mut self) {
220        self.children.clear();
221
222        let cf = Rc::clone(&self.cancel_flag);
223        let sf = Rc::clone(&self.select_flag);
224
225        let cancel = Button::new("Cancel", Arc::clone(&self.font))
226            .on_click(move || cf.set(true));
227        let select = Button::new("Select", Arc::clone(&self.font))
228            .on_click(move || sf.set(true));
229
230        if self.allow_none {
231            let none_check = Checkbox::new(
232                "No Color (Pass Through)",
233                Arc::clone(&self.font),
234                self.no_color,
235            )
236                .with_font_size(self.font_size)
237                .with_state_cell(Rc::clone(&self.none_cell));
238            self.children.push(Box::new(none_check));
239            self.idx_none = Some(0);
240            self.idx_cancel = 1;
241            self.idx_select = 2;
242        } else {
243            self.idx_none = None;
244            self.idx_cancel = 0;
245            self.idx_select = 1;
246        }
247        self.children.push(Box::new(cancel));
248        self.children.push(Box::new(select));
249    }
250
251    fn sync_color_from_hsva(&self) -> Color {
252        if self.no_color {
253            Color::transparent()
254        } else {
255            let (r, g, b) = hsv_to_rgb(self.h, self.s, self.v);
256            Color::rgba(r, g, b, self.a)
257        }
258    }
259
260    fn commit(&mut self) {
261        let c = self.sync_color_from_hsva();
262        self.color_cell.set(c);
263        if let Some(cb) = self.on_select.as_mut() { cb(c); }
264        self.open = false;
265    }
266
267    fn cancel(&mut self) {
268        self.color_cell.set(self.saved);
269        let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
270        self.h = h; self.s = s; self.v = v;
271        self.a = self.saved.a;
272        self.no_color = self.saved.a <= 0.0;
273        self.none_cell.set(self.no_color);
274        self.open = false;
275    }
276
277    /// Local-coord rect for each interactive region of the open panel.
278    /// Y-up: swatch is at the TOP, panel grows DOWNWARD below it in the
279    /// visual sense → higher Y values for the swatch, lower for buttons.
280    fn regions(&self) -> PanelRegions {
281        let w = self.bounds.width;
282        let h = self.bounds.height;
283
284        let swatch = Rect::new(0.0, h - SWATCH_H, w, SWATCH_H);
285
286        // Panel top starts just below the swatch (Y-up → smaller Y).
287        let mut y = h - SWATCH_H - PAD;
288
289        y -= HUE_H;
290        let hue = Rect::new(PAD, y, w - PAD * 2.0, HUE_H);
291        y -= ROW_GAP;
292
293        y -= SV_H;
294        let sv  = Rect::new(PAD, y, w - PAD * 2.0, SV_H);
295        y -= ROW_GAP;
296
297        y -= ALPHA_H;
298        let alpha = Rect::new(PAD, y, w - PAD * 2.0, ALPHA_H);
299        y -= ROW_GAP;
300
301        y -= HEX_H;
302        let hex = Rect::new(PAD, y, w - PAD * 2.0, HEX_H);
303        y -= ROW_GAP;
304
305        let none = if self.allow_none {
306            y -= CHECK_H;
307            let r = Rect::new(PAD, y, w - PAD * 2.0, CHECK_H);
308            Some(r)
309        } else { None };
310        let _ = y;
311
312        let btns_y = PAD;
313        let btn_w  = (w - PAD * 3.0) * 0.5;
314        let cancel = Rect::new(PAD,                  btns_y, btn_w, BTN_H);
315        let select = Rect::new(PAD + btn_w + PAD,    btns_y, btn_w, BTN_H);
316
317        PanelRegions { swatch, hue, sv, alpha, hex, none, cancel, select }
318    }
319}
320
321struct PanelRegions {
322    swatch: Rect,
323    hue:    Rect,
324    sv:     Rect,
325    alpha:  Rect,
326    hex:    Rect,
327    none:   Option<Rect>,
328    cancel: Rect,
329    select: Rect,
330}
331
332impl Widget for ColorPicker {
333    fn type_name(&self) -> &'static str { "ColorPicker" }
334    fn bounds(&self) -> Rect { self.bounds }
335    fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
336    fn children(&self) -> &[Box<dyn Widget>] { &self.children }
337    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
338
339    fn margin(&self)   -> Insets  { self.base.margin }
340    fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
341    fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
342    fn min_size(&self) -> Size    { self.base.min_size }
343    fn max_size(&self) -> Size    { self.base.max_size }
344
345    fn layout(&mut self, available: Size) -> Size {
346        // Sync no_color from cell if someone else flipped it.
347        self.no_color = self.none_cell.get();
348
349        let w = if self.open {
350            PANEL_W.min(available.width.max(PANEL_W))
351        } else {
352            available.width.max(SWATCH_MIN_W).min(PANEL_W)
353        };
354
355        let h = if self.open {
356            SWATCH_H + panel_body_h(self.allow_none)
357        } else {
358            SWATCH_H
359        };
360
361        self.bounds = Rect::new(0.0, 0.0, w, h);
362
363        if self.open {
364            let r = self.regions();
365            // Position sub-widgets.
366            if let Some(none_rect) = r.none {
367                if let Some(idx) = self.idx_none {
368                    let cb = &mut self.children[idx];
369                    cb.layout(Size::new(none_rect.width, none_rect.height));
370                    cb.set_bounds(none_rect);
371                }
372            }
373            let cb = &mut self.children[self.idx_cancel];
374            cb.layout(Size::new(r.cancel.width, r.cancel.height));
375            cb.set_bounds(r.cancel);
376
377            let sb = &mut self.children[self.idx_select];
378            sb.layout(Size::new(r.select.width, r.select.height));
379            sb.set_bounds(r.select);
380        } else {
381            // Give children zero bounds off-panel so they don't paint.
382            for c in self.children.iter_mut() {
383                c.set_bounds(Rect::new(0.0, 0.0, 0.0, 0.0));
384            }
385        }
386
387        Size::new(w, h)
388    }
389
390    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
391        let v = ctx.visuals();
392        let r = self.regions();
393
394        // Outer panel background (if open).
395        if self.open {
396            ctx.set_fill_color(v.widget_bg);
397            ctx.begin_path();
398            ctx.rounded_rect(0.0, 0.0, self.bounds.width, self.bounds.height, 6.0);
399            ctx.fill();
400            ctx.set_stroke_color(v.widget_stroke);
401            ctx.set_line_width(1.0);
402            ctx.begin_path();
403            ctx.rounded_rect(0.0, 0.0, self.bounds.width, self.bounds.height, 6.0);
404            ctx.stroke();
405        }
406
407        // ── Swatch ──────────────────────────────────────────────────────────
408        paint_checker_bg(ctx, r.swatch, 6.0);
409        let cur = self.sync_color_from_hsva();
410        ctx.set_fill_color(cur);
411        ctx.begin_path();
412        ctx.rounded_rect(r.swatch.x, r.swatch.y, r.swatch.width, r.swatch.height, 4.0);
413        ctx.fill();
414        ctx.set_stroke_color(v.widget_stroke);
415        ctx.set_line_width(1.0);
416        ctx.begin_path();
417        ctx.rounded_rect(r.swatch.x, r.swatch.y, r.swatch.width, r.swatch.height, 4.0);
418        ctx.stroke();
419
420        if !self.open { return; }
421
422        // ── Hue slider ──────────────────────────────────────────────────────
423        paint_hue_strip(ctx, r.hue);
424        paint_vertical_marker(ctx, r.hue, self.h, v.widget_stroke_active);
425
426        // ── SV rectangle ────────────────────────────────────────────────────
427        paint_sv_rect(ctx, r.sv, self.h);
428        let mx = r.sv.x + self.s as f64 * r.sv.width;
429        // Y-up: value 1 → TOP of the rect (y = r.sv.y + r.sv.height).
430        let my = r.sv.y + self.v as f64 * r.sv.height;
431        paint_crosshair(ctx, mx, my, v.widget_stroke_active);
432
433        // ── Alpha slider ────────────────────────────────────────────────────
434        paint_checker_bg(ctx, r.alpha, 4.0);
435        paint_alpha_strip(ctx, r.alpha, cur);
436        paint_vertical_marker(ctx, r.alpha, self.a, v.widget_stroke_active);
437
438        // ── Hex readout ─────────────────────────────────────────────────────
439        ctx.set_fill_color(v.widget_bg_hovered);
440        ctx.begin_path();
441        ctx.rounded_rect(r.hex.x, r.hex.y, r.hex.width, r.hex.height, 3.0);
442        ctx.fill();
443        ctx.set_stroke_color(v.widget_stroke);
444        ctx.set_line_width(1.0);
445        ctx.begin_path();
446        ctx.rounded_rect(r.hex.x, r.hex.y, r.hex.width, r.hex.height, 3.0);
447        ctx.stroke();
448
449        let hex = format_hex(cur);
450        ctx.set_font(Arc::clone(&self.font));
451        ctx.set_font_size(self.font_size);
452        ctx.set_fill_color(v.text_color);
453        let text_w = ctx.measure_text(&hex).map(|m| m.width).unwrap_or(0.0);
454        let tx = r.hex.x + (r.hex.width - text_w) * 0.5;
455        let ty = r.hex.y + (r.hex.height - self.font_size) * 0.5 + 2.0;
456        ctx.fill_text(&hex, tx, ty);
457
458        // ── Sub-widgets (No Color + Cancel/Select) ───────────────────────────
459        for child in self.children.iter_mut() {
460            let b = child.bounds();
461            if b.width <= 0.0 || b.height <= 0.0 { continue; }
462            ctx.save();
463            ctx.translate(b.x, b.y);
464            paint_subtree(child.as_mut(), ctx);
465            ctx.restore();
466        }
467    }
468
469    fn on_event(&mut self, event: &Event) -> EventResult {
470        // Let sub-widgets (No Color, Cancel, Select) see pointer events that
471        // actually land on them.  `Button::on_event` consumes every
472        // MouseDown / MouseUp regardless of hit-test, so we MUST gate by
473        // `contains(child.bounds, pos)` before dispatching — otherwise the
474        // Cancel/Select buttons swallow clicks anywhere else in the panel
475        // (hue / sv / alpha), breaking colour picking.
476        if self.open {
477            let local_pt = match event {
478                Event::MouseMove { pos }  => Some(*pos),
479                Event::MouseDown { pos, .. } => Some(*pos),
480                Event::MouseUp   { pos, .. } => Some(*pos),
481                _ => None,
482            };
483
484            for child in self.children.iter_mut() {
485                let b = child.bounds();
486                if b.width <= 0.0 || b.height <= 0.0 { continue; }
487                // Only dispatch pointer events when the cursor is over the
488                // child.  Non-pointer events (focus, keys) we always route,
489                // since the framework already knows who should receive them.
490                if let Some(p) = local_pt {
491                    if !contains(&b, p) { continue; }
492                    let lp = Point::new(p.x - b.x, p.y - b.y);
493                    let translated = translate_mouse_event(event, lp);
494                    let res = child.on_event(&translated);
495                    if res == EventResult::Consumed {
496                        self.handle_btn_flags();
497                        return EventResult::Consumed;
498                    }
499                } else {
500                    let res = child.on_event(event);
501                    if res == EventResult::Consumed {
502                        self.handle_btn_flags();
503                        return EventResult::Consumed;
504                    }
505                }
506            }
507            self.handle_btn_flags();
508        }
509
510        match event {
511            Event::MouseDown { button: MouseButton::Left, pos, .. } => {
512                let r = self.regions();
513                if !self.open {
514                    if contains(&r.swatch, *pos) {
515                        // Open.
516                        self.open = true;
517                        self.saved = self.color_cell.get();
518                        let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
519                        self.h = h; self.s = s; self.v = v; self.a = self.saved.a;
520                        self.no_color = self.saved.a <= 0.0;
521                        self.none_cell.set(self.no_color);
522                        crate::animation::request_tick();
523                        return EventResult::Consumed;
524                    }
525                    return EventResult::Ignored;
526                }
527                if contains(&r.hue, *pos) {
528                    self.drag = Drag::Hue;
529                    self.h = ((pos.x - r.hue.x) / r.hue.width).clamp(0.0, 1.0) as f32;
530                    crate::animation::request_tick();
531                    return EventResult::Consumed;
532                }
533                if contains(&r.sv, *pos) {
534                    self.drag = Drag::Sv;
535                    self.s = ((pos.x - r.sv.x) / r.sv.width).clamp(0.0, 1.0) as f32;
536                    self.v = ((pos.y - r.sv.y) / r.sv.height).clamp(0.0, 1.0) as f32;
537                    crate::animation::request_tick();
538                    return EventResult::Consumed;
539                }
540                if contains(&r.alpha, *pos) {
541                    self.drag = Drag::Alpha;
542                    self.a = ((pos.x - r.alpha.x) / r.alpha.width).clamp(0.0, 1.0) as f32;
543                    crate::animation::request_tick();
544                    return EventResult::Consumed;
545                }
546                EventResult::Ignored
547            }
548            Event::MouseMove { pos } => {
549                self.hovered = self.hit_test(*pos);
550                if self.drag == Drag::None { return EventResult::Ignored; }
551                let r = self.regions();
552                match self.drag {
553                    Drag::Hue => {
554                        self.h = ((pos.x - r.hue.x) / r.hue.width).clamp(0.0, 1.0) as f32;
555                    }
556                    Drag::Sv => {
557                        self.s = ((pos.x - r.sv.x) / r.sv.width).clamp(0.0, 1.0) as f32;
558                        self.v = ((pos.y - r.sv.y) / r.sv.height).clamp(0.0, 1.0) as f32;
559                    }
560                    Drag::Alpha => {
561                        self.a = ((pos.x - r.alpha.x) / r.alpha.width).clamp(0.0, 1.0) as f32;
562                    }
563                    Drag::None => {}
564                }
565                // Live-preview: push working colour to the cell so the demo's
566                // preview updates as the user drags.  Cancel restores `saved`.
567                if !self.no_color {
568                    let c = self.sync_color_from_hsva();
569                    self.color_cell.set(c);
570                }
571                crate::animation::request_tick();
572                EventResult::Consumed
573            }
574            Event::MouseUp { button: MouseButton::Left, .. } => {
575                let was_dragging = self.drag != Drag::None;
576                self.drag = Drag::None;
577                if was_dragging { EventResult::Consumed } else { EventResult::Ignored }
578            }
579            _ => EventResult::Ignored,
580        }
581    }
582}
583
584impl ColorPicker {
585    fn handle_btn_flags(&mut self) {
586        if self.cancel_flag.get() {
587            self.cancel_flag.set(false);
588            self.cancel();
589        }
590        if self.select_flag.get() {
591            self.select_flag.set(false);
592            self.commit();
593        }
594        if self.open {
595            let want = self.none_cell.get();
596            if want != self.no_color {
597                self.no_color = want;
598                if want {
599                    // Preview transparent immediately.
600                    self.color_cell.set(Color::transparent());
601                } else {
602                    let c = self.sync_color_from_hsva();
603                    self.color_cell.set(c);
604                }
605                crate::animation::request_tick();
606            }
607        }
608    }
609}
610
611// ── Drawing helpers ──────────────────────────────────────────────────────────
612
613fn contains(r: &Rect, p: Point) -> bool {
614    p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height
615}
616
617fn translate_mouse_event(e: &Event, p: Point) -> Event {
618    match e {
619        Event::MouseMove { .. } => Event::MouseMove { pos: p },
620        Event::MouseDown { button, modifiers, .. } =>
621            Event::MouseDown { button: *button, pos: p, modifiers: *modifiers },
622        Event::MouseUp { button, modifiers, .. } =>
623            Event::MouseUp { button: *button, pos: p, modifiers: *modifiers },
624        _ => e.clone(),
625    }
626}
627
628fn paint_checker_bg(ctx: &mut dyn DrawCtx, r: Rect, tile: f64) {
629    ctx.set_fill_color(Color::rgb(0.75, 0.75, 0.75));
630    ctx.begin_path();
631    ctx.rect(r.x, r.y, r.width, r.height);
632    ctx.fill();
633
634    ctx.set_fill_color(Color::rgb(0.45, 0.45, 0.45));
635    let cols = (r.width / tile).ceil() as i32;
636    let rows = (r.height / tile).ceil() as i32;
637    for row in 0..rows {
638        for col in 0..cols {
639            if (row + col) & 1 == 0 {
640                let x = r.x + col as f64 * tile;
641                let y = r.y + row as f64 * tile;
642                let w = (tile).min(r.x + r.width - x).max(0.0);
643                let h = (tile).min(r.y + r.height - y).max(0.0);
644                if w > 0.0 && h > 0.0 {
645                    ctx.begin_path();
646                    ctx.rect(x, y, w, h);
647                    ctx.fill();
648                }
649            }
650        }
651    }
652}
653
654fn paint_hue_strip(ctx: &mut dyn DrawCtx, r: Rect) {
655    let steps = r.width.ceil() as i32;
656    let step_w = r.width / steps as f64;
657    for i in 0..steps {
658        let t = i as f32 / steps as f32;
659        let (cr, cg, cb) = hsv_to_rgb(t, 1.0, 1.0);
660        ctx.set_fill_color(Color::rgb(cr, cg, cb));
661        ctx.begin_path();
662        ctx.rect(r.x + i as f64 * step_w, r.y, step_w + 1.0, r.height);
663        ctx.fill();
664    }
665}
666
667fn paint_sv_rect(ctx: &mut dyn DrawCtx, r: Rect, hue: f32) {
668    // Paint horizontal strips from bottom (value=0 → black) to top (value=1 →
669    // fully saturated hue), each strip ramps saturation left→right.
670    // We approximate by stacking thin horizontal strips where each strip
671    // interpolates from white→hue_at_row along X, then multiplying by value.
672    //
673    // Simpler two-pass approximation:
674    //   1. Draw a hue→white horizontal gradient at full V.
675    //   2. Overlay a black→transparent vertical gradient (from bottom).
676    let (hr, hg, hb) = hsv_to_rgb(hue, 1.0, 1.0);
677
678    let cols = r.width.ceil() as i32;
679    let col_w = r.width / cols as f64;
680    for i in 0..cols {
681        let tx = i as f32 / cols as f32;
682        // Hue colour at saturation=tx, value=1
683        let cr = 1.0 * (1.0 - tx) + hr * tx;
684        let cg = 1.0 * (1.0 - tx) + hg * tx;
685        let cb = 1.0 * (1.0 - tx) + hb * tx;
686        ctx.set_fill_color(Color::rgb(cr, cg, cb));
687        ctx.begin_path();
688        ctx.rect(r.x + i as f64 * col_w, r.y, col_w + 1.0, r.height);
689        ctx.fill();
690    }
691
692    // Vertical value overlay: black (bottom) → transparent (top).
693    let rows = r.height.ceil() as i32;
694    let row_h = r.height / rows as f64;
695    for j in 0..rows {
696        // Y-up: j=0 is the BOTTOM row (value=0 → full black).
697        let ty = j as f32 / rows as f32; // 0 at bottom, 1 at top
698        let alpha = 1.0 - ty;
699        ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, alpha));
700        ctx.begin_path();
701        ctx.rect(r.x, r.y + j as f64 * row_h, r.width, row_h + 1.0);
702        ctx.fill();
703    }
704}
705
706fn paint_alpha_strip(ctx: &mut dyn DrawCtx, r: Rect, c: Color) {
707    let steps = r.width.ceil() as i32;
708    let step_w = r.width / steps as f64;
709    for i in 0..steps {
710        let t = i as f32 / steps as f32;
711        ctx.set_fill_color(Color::rgba(c.r, c.g, c.b, t));
712        ctx.begin_path();
713        ctx.rect(r.x + i as f64 * step_w, r.y, step_w + 1.0, r.height);
714        ctx.fill();
715    }
716}
717
718fn paint_vertical_marker(ctx: &mut dyn DrawCtx, r: Rect, t: f32, col: Color) {
719    let x = r.x + (t.clamp(0.0, 1.0) as f64) * r.width;
720    ctx.set_stroke_color(Color::white());
721    ctx.set_line_width(3.0);
722    ctx.begin_path();
723    ctx.move_to(x, r.y);
724    ctx.line_to(x, r.y + r.height);
725    ctx.stroke();
726    ctx.set_stroke_color(col);
727    ctx.set_line_width(1.5);
728    ctx.begin_path();
729    ctx.move_to(x, r.y);
730    ctx.line_to(x, r.y + r.height);
731    ctx.stroke();
732}
733
734fn paint_crosshair(ctx: &mut dyn DrawCtx, x: f64, y: f64, col: Color) {
735    ctx.set_stroke_color(Color::white());
736    ctx.set_line_width(3.0);
737    ctx.begin_path();
738    ctx.circle(x, y, 5.0);
739    ctx.stroke();
740    ctx.set_stroke_color(col);
741    ctx.set_line_width(1.5);
742    ctx.begin_path();
743    ctx.circle(x, y, 5.0);
744    ctx.stroke();
745}