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::{paint_subtree, Widget};
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 {
64        h += CHECK_H + ROW_GAP;
65    }
66    h += BTN_H + PAD;
67    h
68}
69
70// ── HSV / RGB helpers ────────────────────────────────────────────────────────
71
72fn rgb_to_hsv(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
73    let max = r.max(g).max(b);
74    let min = r.min(g).min(b);
75    let d = max - min;
76    let v = max;
77    let s = if max <= 0.0 { 0.0 } else { d / max };
78    let h = if d <= 0.0 {
79        0.0
80    } else if max == r {
81        60.0 * (((g - b) / d) % 6.0)
82    } else if max == g {
83        60.0 * (((b - r) / d) + 2.0)
84    } else {
85        60.0 * (((r - g) / d) + 4.0)
86    };
87    let h = if h < 0.0 { h + 360.0 } else { h };
88    (h / 360.0, s, v)
89}
90
91fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
92    let h6 = (h * 6.0) % 6.0;
93    let c = v * s;
94    let x = c * (1.0 - (h6 % 2.0 - 1.0).abs());
95    let (r1, g1, b1) = match h6 as i32 {
96        0 => (c, x, 0.0),
97        1 => (x, c, 0.0),
98        2 => (0.0, c, x),
99        3 => (0.0, x, c),
100        4 => (x, 0.0, c),
101        _ => (c, 0.0, x),
102    };
103    let m = v - c;
104    (r1 + m, g1 + m, b1 + m)
105}
106
107fn format_hex(c: Color) -> String {
108    let r = (c.r * 255.0).clamp(0.0, 255.0) as u32;
109    let g = (c.g * 255.0).clamp(0.0, 255.0) as u32;
110    let b = (c.b * 255.0).clamp(0.0, 255.0) as u32;
111    let a = (c.a * 255.0).clamp(0.0, 255.0) as u32;
112    format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
113}
114
115// ── Drag mode ────────────────────────────────────────────────────────────────
116
117#[derive(Clone, Copy, Debug, PartialEq)]
118enum Drag {
119    None,
120    Hue,
121    Sv,
122    Alpha,
123}
124
125// ── Widget ───────────────────────────────────────────────────────────────────
126
127/// Inline colour picker bound to a shared `Color` cell.
128pub struct ColorPicker {
129    bounds: Rect,
130    children: Vec<Box<dyn Widget>>, // [no_color_check?, cancel, select]
131    base: WidgetBase,
132
133    font: Arc<Font>,
134    font_size: f64,
135
136    /// Authoritative colour the caller observes.  Only written on Select (or
137    /// when "No Color" toggles, depending on wiring).
138    color_cell: Rc<Cell<Color>>,
139
140    /// Snapshot taken when the picker was opened — restored on Cancel.
141    saved: Color,
142
143    /// Working state while the panel is open.
144    open: bool,
145    h: f32,
146    s: f32,
147    v: f32,
148    a: f32,
149    /// True when "No Color (Pass Through)" is checked — working state; applied
150    /// to the cell on Select as `Color::transparent()`.
151    no_color: bool,
152    allow_none: bool,
153
154    /// None means not currently dragging anything.
155    drag: Drag,
156
157    /// Last local mouse position — fed into child widget layout for hit tests.
158    hovered: bool,
159
160    /// Optional callback invoked on Select with the final colour.
161    on_select: Option<Box<dyn FnMut(Color)>>,
162
163    // ── Sub-widget indices into `children` ───────────────────────────────────
164    /// Set during `build_children` so paint/layout can find them quickly.
165    idx_cancel: usize,
166    idx_select: usize,
167    idx_none: Option<usize>,
168
169    /// Shared "no color" checkbox state.  Owned by `ColorPicker` so `on_event`
170    /// can react to changes without going through a callback chain.
171    none_cell: Rc<Cell<bool>>,
172    /// Shared flags the sub-buttons flip; read + cleared by `on_event`.
173    cancel_flag: Rc<Cell<bool>>,
174    select_flag: Rc<Cell<bool>>,
175}
176
177impl ColorPicker {
178    pub fn new(color_cell: Rc<Cell<Color>>, font: Arc<Font>) -> Self {
179        let initial = color_cell.get();
180        let (h, s, v) = rgb_to_hsv(initial.r, initial.g, initial.b);
181        let none_cell = Rc::new(Cell::new(false));
182        let cancel_flag = Rc::new(Cell::new(false));
183        let select_flag = Rc::new(Cell::new(false));
184
185        let mut me = Self {
186            bounds: Rect::default(),
187            children: Vec::new(),
188            base: WidgetBase::new(),
189            font: Arc::clone(&font),
190            font_size: 13.0,
191            color_cell,
192            saved: initial,
193            open: false,
194            h,
195            s,
196            v,
197            a: initial.a,
198            no_color: initial.a <= 0.0,
199            allow_none: false,
200            drag: Drag::None,
201            hovered: false,
202            on_select: None,
203            idx_cancel: 0,
204            idx_select: 1,
205            idx_none: None,
206            none_cell,
207            cancel_flag,
208            select_flag,
209        };
210        me.build_children();
211        me
212    }
213
214    pub fn with_font_size(mut self, s: f64) -> Self {
215        self.font_size = s;
216        self
217    }
218    pub fn with_allow_none(mut self, allow: bool) -> Self {
219        self.allow_none = allow;
220        self.build_children();
221        self
222    }
223    pub fn on_select(mut self, cb: impl FnMut(Color) + 'static) -> Self {
224        self.on_select = Some(Box::new(cb));
225        self
226    }
227
228    pub fn with_margin(mut self, m: Insets) -> Self {
229        self.base.margin = m;
230        self
231    }
232    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
233        self.base.h_anchor = h;
234        self
235    }
236    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
237        self.base.v_anchor = v;
238        self
239    }
240    pub fn with_min_size(mut self, s: Size) -> Self {
241        self.base.min_size = s;
242        self
243    }
244    pub fn with_max_size(mut self, s: Size) -> Self {
245        self.base.max_size = s;
246        self
247    }
248
249    fn build_children(&mut self) {
250        self.children.clear();
251
252        let cf = Rc::clone(&self.cancel_flag);
253        let sf = Rc::clone(&self.select_flag);
254
255        let cancel = Button::new("Cancel", Arc::clone(&self.font)).on_click(move || cf.set(true));
256        let select = Button::new("Select", Arc::clone(&self.font)).on_click(move || sf.set(true));
257
258        if self.allow_none {
259            let none_check = Checkbox::new(
260                "No Color (Pass Through)",
261                Arc::clone(&self.font),
262                self.no_color,
263            )
264            .with_font_size(self.font_size)
265            .with_state_cell(Rc::clone(&self.none_cell));
266            self.children.push(Box::new(none_check));
267            self.idx_none = Some(0);
268            self.idx_cancel = 1;
269            self.idx_select = 2;
270        } else {
271            self.idx_none = None;
272            self.idx_cancel = 0;
273            self.idx_select = 1;
274        }
275        self.children.push(Box::new(cancel));
276        self.children.push(Box::new(select));
277    }
278
279    fn sync_color_from_hsva(&self) -> Color {
280        if self.no_color {
281            Color::transparent()
282        } else {
283            let (r, g, b) = hsv_to_rgb(self.h, self.s, self.v);
284            Color::rgba(r, g, b, self.a)
285        }
286    }
287
288    fn commit(&mut self) {
289        let c = self.sync_color_from_hsva();
290        self.color_cell.set(c);
291        if let Some(cb) = self.on_select.as_mut() {
292            cb(c);
293        }
294        self.open = false;
295    }
296
297    fn cancel(&mut self) {
298        self.color_cell.set(self.saved);
299        let (h, s, v) = rgb_to_hsv(self.saved.r, self.saved.g, self.saved.b);
300        self.h = h;
301        self.s = s;
302        self.v = v;
303        self.a = self.saved.a;
304        self.no_color = self.saved.a <= 0.0;
305        self.none_cell.set(self.no_color);
306        self.open = false;
307    }
308
309    /// Local-coord rect for each interactive region of the open panel.
310    /// Y-up: swatch is at the TOP, panel grows DOWNWARD below it in the
311    /// visual sense → higher Y values for the swatch, lower for buttons.
312    fn regions(&self) -> PanelRegions {
313        let w = self.bounds.width;
314        let h = self.bounds.height;
315
316        let swatch = Rect::new(0.0, h - SWATCH_H, w, SWATCH_H);
317
318        // Panel top starts just below the swatch (Y-up → smaller Y).
319        let mut y = h - SWATCH_H - PAD;
320
321        y -= HUE_H;
322        let hue = Rect::new(PAD, y, w - PAD * 2.0, HUE_H);
323        y -= ROW_GAP;
324
325        y -= SV_H;
326        let sv = Rect::new(PAD, y, w - PAD * 2.0, SV_H);
327        y -= ROW_GAP;
328
329        y -= ALPHA_H;
330        let alpha = Rect::new(PAD, y, w - PAD * 2.0, ALPHA_H);
331        y -= ROW_GAP;
332
333        y -= HEX_H;
334        let hex = Rect::new(PAD, y, w - PAD * 2.0, HEX_H);
335        y -= ROW_GAP;
336
337        let none = if self.allow_none {
338            y -= CHECK_H;
339            let r = Rect::new(PAD, y, w - PAD * 2.0, CHECK_H);
340            Some(r)
341        } else {
342            None
343        };
344        let _ = y;
345
346        let btns_y = PAD;
347        let btn_w = (w - PAD * 3.0) * 0.5;
348        let cancel = Rect::new(PAD, btns_y, btn_w, BTN_H);
349        let select = Rect::new(PAD + btn_w + PAD, btns_y, btn_w, BTN_H);
350
351        PanelRegions {
352            swatch,
353            hue,
354            sv,
355            alpha,
356            hex,
357            none,
358            cancel,
359            select,
360        }
361    }
362}
363
364struct PanelRegions {
365    swatch: Rect,
366    hue: Rect,
367    sv: Rect,
368    alpha: Rect,
369    hex: Rect,
370    none: Option<Rect>,
371    cancel: Rect,
372    select: Rect,
373}
374
375mod widget_impl;