Skip to main content

ui/
color.rs

1// color -- colour swatches, the colour picker and the HSV colour wheel
2//                                                     [src/ui/ui_color.s, src/ui/ui_wheel.s]
3//
4//   swatch(id, argb, selected) -> EV        round colour well (22 DIP or next_size); alpha 0
5//        draws an empty slot; EV_ACT on click, EV_RCLICK on right click
6//   color_picker(id, &mut argb) -> EV       spectrum (saturation x value), hue slider, preview
7//        and hex text; EV_ACT while the colour changes, EV_COMMIT on release.  Uses the ids
8//        id + 0x20000000 (spectrum) and id + 0x10000000 (hue) for its parts.
9//   color_picker_ex(id, &mut argb, flags) -> EV   (U1) the same with CP_ALPHA: an alpha bar
10//        (id + 0x30000000, checkerboard) under the hue bar; CP_FIELDS: number boxes R G B (A)
11//        and H S V (part_id(id, 3..9)) and a hex text box (part_id(id, 10); RRGGBB,
12//        AARRGGBB with CP_ALPHA, taken on Enter / focus loss) instead of the hex text;
13//        CP_PIPETTE: a pipette button beside the preview (part_id(id, 11); only where the
14//        user can pick a colour anywhere on the screen, screen.rs): the picked colour (alpha
15//        kept) comes with EV_ACT | EV_COMMIT on the picker's next pass.
16//   color_wheel(id, &mut color, flags) -> EV  (U1) hue around the circle (0 = red at the
17//        right, counter-clockwise), saturation from the centre to the rim; the value is not
18//        part of the wheel (a separate track slider) but the wheel is drawn at that value.
19//        color: [0xAARRGGBB] (alpha kept; the HSV state lives in a state slot so greys keep
20//        their hue), or with CW_HSV an HSV record [h 0..3599, s 0..1000, v 0..1000].  Drag
21//        sets hue and saturation; keys: Left / Right hue -/+ 1 degree, Up / Down saturation
22//        +/- 1 % (Shift: x10).  EV_ACT while it changes, EV_COMMIT on release / keys.
23//        Natural size 160 DIP.  Semantic record: K_SLIDER, value = hue, max = saturation.
24//   hsv_to_rgb(h, s, v) -> 0xFFRRGGBB       h 0..3599 (tenths of a degree), s / v 0..1000
25//   rgb_to_hsv(rgb) -> (h, s, v)            inverse (same ranges)
26//
27// The wheel image is cached per widget (WC_MAX entries, shared by all windows): a
28// premultiplied image at full value for the current size (antialiased rim), and the image
29// scaled to the current value; only a change of size or value re-renders it.
30
31use alloc::collections::BTreeMap;
32use alloc::format;
33use alloc::rc::Rc;
34use alloc::string::String;
35
36use super::cmd::{Draw, Gfx};
37use super::consts::*;
38use super::keys::*;
39use super::value::{deg_atan2, f_round, part_id, px_mix, Track};
40use super::{Sp, Ui};
41use gfx::{Surface, FMT_PARGB};
42use rt::fx::{cvtd, minsd, sqrtd};
43
44const PART_SPEC: u32 = 0x2000_0000;
45const PART_HUE: u32 = 0x1000_0000;
46const PART_ALPHA: u32 = 0x3000_0000;
47const WHEEL_NAT: i32 = 160; // natural size of the wheel (DIP)
48const WC_MAX: usize = 4; // cached wheels
49
50const RG_BYTE: Range = Range::new(0, 255, 1, 0);
51const RG_HUE: Range = Range::new(0, 359, 1, 0);
52const RG_PCT: Range = Range::new(0, 100, 1, 0);
53const FLD_LEFT: [&str; 4] = ["R", "G", "B", "A"];
54const FLD_RIGHT: [&str; 4] = ["H", "S", "V", "Hex"];
55
56// the field store of a picker with CP_FIELDS (the asm keeps it in the slot of
57// part_id(id, 15)): R G B A, H S V, the hex text
58#[derive(Default)]
59struct Fields {
60    rgba: [i32; 4],
61    hsv: [i32; 3],
62    hex: String,
63}
64
65// a cached wheel  [WC_*]
66#[derive(Default)]
67struct Wc {
68    id: u32,
69    /// side in px of the cached images (0 = none)
70    size: i32,
71    /// value of the display image (-1 = stale)
72    v: i32,
73    seen: u32,
74    ver: u32,
75    base: Surface,
76    disp: Rc<Surface>,
77}
78
79/// state shared by all windows
80#[derive(Default)]
81pub(crate) struct S {
82    fields: BTreeMap<u32, Fields>,
83    wc: [Wc; WC_MAX],
84    wc_ver: u32,
85    spec: Option<Rc<SpectrumDraw>>,
86    hue: Option<Rc<HueDraw>>,
87}
88
89// ------------------------------------------------------------------ HSV
90
91/// hsv_to_rgb(h, s, v) -> 0xFFRRGGBB   (h 0..3599, s / v 0..1000)
92pub fn hsv_to_rgb(h: u32, s: u32, v: u32) -> u32 {
93    // V = v * 255 / 1000
94    let vv = v.wrapping_mul(255).wrapping_add(500) / 1000;
95    if s == 0 {
96        return vv.wrapping_mul(0x01_0101) | 0xFF00_0000;
97    }
98    // sector and fraction
99    let mut sec = h / 600;
100    let f = h % 600;
101    if sec > 5 {
102        sec = 0;
103    }
104    // p = V (1000 - s) / 1000, q = V (600000 - s f) / 600000, t = V (600000 - s (600 - f)) / 600000
105    let p = 1000u32.wrapping_sub(s).wrapping_mul(vv).wrapping_add(500) / 1000;
106    let q = 600_000u32.wrapping_sub(s.wrapping_mul(f)).wrapping_mul(vv).wrapping_add(300_000) / 600_000;
107    let t = 600_000u32.wrapping_sub(600u32.wrapping_sub(f).wrapping_mul(s)).wrapping_mul(vv).wrapping_add(300_000) / 600_000;
108    let (r, g, b) = match sec {
109        0 => (vv, t, p),
110        1 => (q, vv, p),
111        2 => (p, vv, t),
112        3 => (p, q, vv),
113        4 => (t, p, vv),
114        _ => (vv, p, q),
115    };
116    r << 16 | g << 8 | b | 0xFF00_0000
117}
118
119/// rgb_to_hsv(rgb) -> (h 0..3599, s 0..1000, v 0..1000)
120pub fn rgb_to_hsv(rgb: u32) -> (u32, u32, u32) {
121    let r = (rgb >> 16) & 255;
122    let g = (rgb >> 8) & 255;
123    let b = rgb & 255;
124    let max = r.max(g).max(b);
125    let min = r.min(g).min(b);
126    // v = max * 1000 / 255
127    let v = (max * 1000 + 127) / 255;
128    let delta = max - min;
129    if max == 0 {
130        return (0, 0, v); // black: h = s = 0
131    }
132    let s = delta * 1000 / max;
133    let mut h: i32 = 0;
134    if delta != 0 {
135        let d = delta as i32;
136        let (r, g, b) = (r as i32, g as i32, b as i32);
137        if max as i32 == r {
138            h = (g - b) * 600 / d;
139            if h < 0 {
140                h += 3600;
141            }
142        } else if max as i32 == g {
143            h = (b - r) * 600 / d + 1200;
144        } else {
145            h = (r - g) * 600 / d + 2400;
146        }
147    }
148    let mut h = h as u32;
149    if h >= 3600 {
150        h -= 3600;
151    }
152    (h, s, v)
153}
154
155// clamp1000(v) -> v in 0..1000
156fn clamp1000(v: i32) -> i32 {
157    v.clamp(0, 1000)
158}
159
160// row of the target at y (the custom drawings write the target pixels directly)
161fn put(g: &mut Gfx, x: i32, y: i32, c: u32) {
162    let o = y as usize * g.sf.stride as usize + x as usize * 4;
163    g.sf.px[o..o + 4].copy_from_slice(&c.to_le_bytes());
164}
165
166/// spectrum_draw: the saturation / value square of a hue (render callback)
167pub(crate) struct SpectrumDraw {
168    hue: u32,
169}
170
171impl Draw for SpectrumDraw {
172    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
173        let k = g.dc.clip;
174        // rows y in [max(ry, cy0), min(ry + rh, cy1))
175        let mut y = r[1].max(k[1]);
176        while y < r[1].wrapping_add(r[3]).min(k[3]) {
177            // v = 1000 - (y - ry) * 1000 / rh
178            let v = 1000 - y.wrapping_sub(r[1]).wrapping_mul(1000) / r[3];
179            let mut x = r[0].max(k[0]);
180            while x < r[0].wrapping_add(r[2]).min(k[2]) {
181                let s = x.wrapping_sub(r[0]).wrapping_mul(1000) / r[2];
182                put(g, x, y, hsv_to_rgb(self.hue, s as u32, v as u32));
183                x += 1;
184            }
185            y += 1;
186        }
187    }
188}
189
190/// hue_draw: the rainbow track (render callback)
191pub(crate) struct HueDraw;
192
193impl Draw for HueDraw {
194    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
195        let k = g.dc.clip;
196        let mut y = r[1].max(k[1]);
197        while y < r[1].wrapping_add(r[3]).min(k[3]) {
198            let mut x = r[0].max(k[0]);
199            while x < r[0].wrapping_add(r[2]).min(k[2]) {
200                let h = x.wrapping_sub(r[0]).wrapping_mul(3599) / r[2];
201                put(g, x, y, hsv_to_rgb(h as u32, 1000, 1000));
202                x += 1;
203            }
204            y += 1;
205        }
206    }
207}
208
209// wc_render_base(base, side): the wheel at full value (premultiplied)
210fn wc_render_base(sf: &mut Surface, side: i32) {
211    let half = side as f64 * 0.5; // centre
212    let rr = half - 1.0; // radius: 1 px inside the square
213    let stride = sf.stride as usize;
214    for y in 0..side {
215        let row = y as usize * stride;
216        // fy = half - (y + 0.5)
217        let fy = half - (y as f64 + 0.5);
218        for x in 0..side {
219            let fx = (x as f64 + 0.5) - half;
220            let r = sqrtd(fx * fx + fy * fy);
221            // coverage = clamp(R + 0.5 - r, 0, 1)
222            let c = (rr + 0.5) - r;
223            let o = row + x as usize * 4;
224            let px = if !(c > 0.0) {
225                0
226            } else {
227                let a = cvtd(minsd(c, 1.0) * 255.0); // alpha 0..255
228                // saturation = min(r / R, 1) * 1000
229                let sat = cvtd(minsd(r / rr, 1.0) * 1000.0);
230                // hue = atan2(fy, fx) in tenths of a degree, 0 .. 3599
231                let mut h = cvtd(deg_atan2(fy, fx) * 10.0);
232                if h < 0 {
233                    h += 3600;
234                }
235                if h as u32 >= 3600 {
236                    h -= 3600;
237                }
238                let c = hsv_to_rgb(h as u32, sat as u32, 1000);
239                // premultiply by the coverage
240                if (a as u32) < 255 {
241                    px_mix(0, c, a as u32) | (a as u32) << 24
242                } else {
243                    c | 0xFF00_0000
244                }
245            };
246            sf.px[o..o + 4].copy_from_slice(&px.to_le_bytes());
247        }
248    }
249}
250
251// wc_render_disp(base, side, v) -> the base with the colour channels * v / 1000
252fn wc_render_disp(base: &Surface, side: i32, v: u32) -> Surface {
253    let mut d = Surface::new(side, side, FMT_PARGB);
254    let (bs, ds) = (base.stride as usize, d.stride as usize);
255    for y in 0..side as usize {
256        for x in 0..side as usize {
257            let o = y * bs + x * 4;
258            let p = u32::from_le_bytes([base.px[o], base.px[o + 1], base.px[o + 2], base.px[o + 3]]);
259            let mut q = p & 0xFF00_0000; // alpha stays
260            for sh in [0, 8, 16] {
261                let c = ((p >> sh) & 255).wrapping_mul(v).wrapping_add(500) / 1000;
262                q |= c << sh;
263            }
264            let o = y * ds + x * 4;
265            d.px[o..o + 4].copy_from_slice(&q.to_le_bytes());
266        }
267    }
268    d
269}
270
271impl Ui {
272    // ------------------------------------------------------------------ swatch
273
274    /// ui_swatch(id, argb, selected) -> EV_*: the swatches of one box are one Tab stop (the
275    /// selected one; arrows move inside, Space / Enter act, the menu key is a right click);
276    /// next_flags(ST_NOFOCUS): a picture only, no Tab stop
277    #[cfg_attr(jfgui_dev, track_caller)]
278    pub fn swatch(&mut self, id: u32, argb: u32, selected: bool) -> u32 {
279        let _src = self.src_scope();
280        let f = self.take_flags();
281        let st = self.style;
282        let mut sp = Sp::default();
283        let (w, h) = st.swatch_size(self, &mut sp);
284        self.place(w, h);
285        let hb = if f & ST_NOFOCUS != 0 {
286            0
287        } else {
288            self.kb_box_group(0x5357);
289            HB_FOCUS | if selected { HB_CURRENT } else { 0 }
290        };
291        let ev = self.behave(id, hb);
292        // paint (style)
293        sp.place(self.wp.ctx.last_rect);
294        sp.ev = ev;
295        sp.value = argb as i32;
296        sp.value2 = selected as i32;
297        st.swatch(self, &sp);
298        self.focus_ring_draw(ev);
299        if selected {
300            self.wp.ctx.last_state |= SS_SELECTED as u32;
301        }
302        self.sem(K_SWATCH, id, None, argb as i32);
303        ev
304    }
305
306    // ------------------------------------------------------------------ colour picker
307
308    /// ui_color_picker(id, &mut argb) -> EV_*
309    #[cfg_attr(jfgui_dev, track_caller)]
310    pub fn color_picker(&mut self, id: u32, argb: &mut u32) -> u32 {
311        let _src = self.src_scope();
312        self.color_picker_ex(id, argb, 0)
313    }
314
315    // the picker's HSV state: slot(id) measw = the colour it belongs to, scrollx / scrolly /
316    // aux = h / s / v
317    fn cp_hsv(&mut self, id: u32) -> (u32, u32, u32) {
318        let s = self.slot(id);
319        (s.scrollx as u32, s.scrolly as u32, s.aux as u32)
320    }
321
322    // .cp_apply: *argb = the HSV state (alpha kept, 0 -> opaque), remembered
323    fn cp_apply(&mut self, id: u32, argb: &mut u32) {
324        let (h, s, v) = self.cp_hsv(id);
325        let rgb = hsv_to_rgb(h, s, v) & 0xFF_FFFF;
326        let mut a = *argb & 0xFF00_0000;
327        if a == 0 {
328            a = 0xFF00_0000;
329        }
330        *argb = rgb | a;
331        self.slot(id).measw = *argb as i32;
332        self.repass();
333    }
334
335    // .cp_marker: the style's ring marker at (x, y) 24.8 px
336    fn cp_marker(&mut self, x: i32, y: i32) {
337        let st = self.style;
338        st.color_marker(self, x, y);
339    }
340
341    /// ui_color_picker_ex(id, &mut argb, flags) -> EV_* (CP_ALPHA, CP_FIELDS, CP_PIPETTE)
342    #[cfg_attr(jfgui_dev, track_caller)]
343    pub fn color_picker_ex(&mut self, id: u32, argb: &mut u32, flags: u32) -> u32 {
344        let _src = self.src_scope();
345        let mut ev = 0;
346        let old = *argb;
347        // a colour the pipette picked for this picker (screen.rs)
348        if let Some((owner, c)) = self.m.screen.pending {
349            if owner == id {
350                self.m.screen.pending = None;
351                let a = if *argb >> 24 == 0 { 0xFF00_0000 } else { *argb & 0xFF00_0000 };
352                *argb = a | (c & 0xFF_FFFF);
353                ev |= EV_COMMIT;
354            }
355        }
356        // resync the HSV state when the colour was changed from outside
357        if *argb as i32 != self.slot(id).measw {
358            let c = *argb;
359            let (h, s, v) = rgb_to_hsv(c);
360            let sl = self.slot(id);
361            sl.measw = c as i32;
362            if s != 0 {
363                sl.scrollx = h as i32; // grey: keep the previous hue
364            }
365            sl.scrolly = s as i32;
366            sl.aux = v as i32;
367        }
368        self.box_begin(0, BOX_V, 12);
369        let rect = self.wp.ctx.last_rect; // (U1) the picker's rectangle for its record
370
371        // ---- spectrum: x = saturation, y = value
372        let (w, h) = (self.dp(256), self.dp(180));
373        let r = self.place(w, h);
374        self.ltr_begin(r[0], r[2]); // right-to-left: colours keep their direction
375        let e = self.behave(id.wrapping_add(PART_SPEC), HB_FOCUS | HB_MOVES | CUR_CROSS << HB_CURSOR_SHIFT);
376        ev |= e;
377        // keyboard: Left / Right saturation, Up / Down brightness (1 %, Shift 10 %)
378        let (dx, dy) = self.cp_keys(e, 10, true);
379        if dx | dy != 0 {
380            let sl = self.slot(id);
381            sl.scrolly = (sl.scrolly + dx).clamp(0, 1000);
382            sl.aux = (sl.aux - dy).clamp(0, 1000);
383            self.cp_apply(id, argb);
384        }
385        if e & EV_HELD != 0 {
386            let r = self.wp.ctx.last_rect;
387            let s = clamp1000(self.ltr_x(self.wp.ctx.mx).wrapping_sub(r[0]).wrapping_mul(1000) / r[2]);
388            let v = 1000 - clamp1000(self.wp.ctx.my.wrapping_sub(r[1]).wrapping_mul(1000) / r[3]);
389            let sl = *self.slot(id);
390            // (U1) only a change repaints: a held pointer must not repass for ever
391            if s != sl.scrolly || v != sl.aux {
392                let sl = self.slot(id);
393                sl.scrolly = s;
394                sl.aux = v;
395                self.cp_apply(id, argb);
396            }
397        }
398        let r = self.wp.ctx.last_rect;
399        let (hue, sat, val) = self.cp_hsv(id);
400        let d = self.spectrum_drawing(hue);
401        self.cmd_custom(r[0], r[1], r[2], r[3], Some(d), hue as usize, hue);
402        // marker ring at (s, v)
403        let mx = ((sat.wrapping_mul(r[2] as u32) / 1000) as i32).wrapping_add(r[0]) << 8;
404        let my = (((1000u32.wrapping_sub(val)).wrapping_mul(r[3] as u32) / 1000) as i32).wrapping_add(r[1]) << 8;
405        self.cp_marker(mx, my);
406        self.cp_part_end(e, id.wrapping_add(PART_SPEC), super::i18n::tr("Sättigung und Helligkeit"), sat as i32, 1000);
407        self.ltr_end();
408
409        // ---- hue slider
410        let (w, h) = (self.dp(256), self.dp(16));
411        let r = self.place(w, h);
412        self.ltr_begin(r[0], r[2]);
413        let e = self.behave(id.wrapping_add(PART_HUE), HB_FOCUS | CUR_HAND << HB_CURSOR_SHIFT | HB_GRAB_H | HB_GRAB_V);
414        ev |= e;
415        // keyboard: arrows 1 degree (Shift 10), Home / End the ends
416        let (dx, _) = self.cp_keys(e, 10, false);
417        if dx != 0 {
418            let sl = self.slot(id);
419            sl.scrollx = (sl.scrollx + dx).clamp(0, 3599);
420            self.cp_apply(id, argb);
421        }
422        if e & EV_HELD != 0 {
423            let r = self.wp.ctx.last_rect;
424            let hh = (self.ltr_x(self.wp.ctx.mx).wrapping_sub(r[0]).wrapping_mul(3599) / r[2]).clamp(0, 3599);
425            if hh != self.slot(id).scrollx {
426                // (U1) only a change repaints
427                self.slot(id).scrollx = hh;
428                self.cp_apply(id, argb);
429            }
430        }
431        let r = self.wp.ctx.last_rect;
432        let d = self.hue_drawing();
433        self.cmd_custom(r[0], r[1], r[2], r[3], Some(d), 0, 0);
434        let hue = self.slot(id).scrollx as u32;
435        let mx = ((hue.wrapping_mul(r[2] as u32) / 3599) as i32).wrapping_add(r[0]) << 8;
436        let my = (r[3] << 7).wrapping_add(r[1] << 8);
437        self.cp_marker(mx, my);
438        self.cp_part_end(e, id.wrapping_add(PART_HUE), super::i18n::tr("Farbton"), hue as i32, 3599);
439        self.ltr_end();
440
441        // ---- (U1) alpha bar: transparent .. opaque over a checkerboard
442        if flags & CP_ALPHA != 0 {
443            let (w, h) = (self.dp(256), self.dp(16));
444            let r = self.place(w, h);
445            self.ltr_begin(r[0], r[2]);
446            let e = self.behave(id.wrapping_add(PART_ALPHA), HB_FOCUS | CUR_HAND << HB_CURSOR_SHIFT | HB_GRAB_H | HB_GRAB_V);
447            ev |= e;
448            // keyboard: arrows 1 of 255 (Shift 16), Home / End the ends
449            let (dx, _) = self.cp_keys(e, 1, false);
450            if dx != 0 {
451                let d = if dx.abs() == 10 { dx / 10 * 16 } else { dx };
452                let a = ((*argb >> 24) as i32 + d).clamp(0, 255) as u32;
453                let na = a << 24 | (*argb & 0xFF_FFFF);
454                if na != *argb {
455                    *argb = na;
456                    self.slot(id).measw = na as i32; // rgb unchanged: the HSV state stays
457                    self.repass();
458                }
459            }
460            if e & EV_HELD != 0 {
461                let r = self.wp.ctx.last_rect;
462                let half = ((r[2] as u32) >> 1) as i32;
463                let a = (self.ltr_x(self.wp.ctx.mx).wrapping_sub(r[0]).wrapping_mul(255).wrapping_add(half) / r[2]).clamp(0, 255);
464                let na = (a as u32) << 24 | (*argb & 0xFF_FFFF);
465                if na != *argb {
466                    *argb = na;
467                    self.slot(id).measw = na as i32; // rgb unchanged: the HSV state stays
468                    self.repass();
469                }
470            }
471            let tk = Track::new(TK_RECT | TK_CHECKER, &[*argb & 0xFF_FFFF, *argb | 0xFF00_0000]);
472            let r = self.wp.ctx.last_rect;
473            self.cmd_track(r[0], r[1], r[2], r[3], &tk);
474            let mx = (((*argb >> 24).wrapping_mul(r[2] as u32) / 255) as i32).wrapping_add(r[0]) << 8;
475            let my = (r[3] << 7).wrapping_add(r[1] << 8);
476            self.cp_marker(mx, my);
477            self.cp_part_end(e, id.wrapping_add(PART_ALPHA), super::i18n::tr("Deckkraft"), (*argb >> 24) as i32, 255);
478            self.ltr_end();
479        }
480
481        // ---- preview + hex
482        self.box_begin(0, BOX_H | BOX_XCENTER, 12);
483        let (w, h) = (self.dp(48), self.dp(28)); // the preview well (style paints it)
484        self.place(w, h);
485        let st = self.style;
486        let r = self.wp.ctx.last_rect;
487        st.color_preview(self, *argb, r);
488        let hex = format!("#{:06X}", *argb & 0xFF_FFFF);
489        if flags & CP_FIELDS == 0 {
490            self.text(TS_BODY, &hex);
491        }
492        // the pipette: a colour from anywhere on the screen (only where that is possible)
493        if flags & CP_PIPETTE != 0 && self.pick_available() {
494            let e = self.icon_button(part_id(id, 11), super::keys::IC_EYEDROPPER, super::i18n::tr("Farbe vom Bildschirm aufnehmen"));
495            if e & EV_ACT != 0 {
496                self.pick_start_for(id);
497            }
498        }
499        self.box_end();
500        if flags & CP_FIELDS != 0 {
501            ev |= self.cp_fields(id, argb, flags);
502        }
503        self.box_end();
504        self.wp.ctx.last_rect = rect;
505        self.wp.ctx.last_state = 0;
506        self.sem(K_SWATCH, id, None, *argb as i32);
507        let mut ret = ev & !EV_ACT;
508        if *argb != old {
509            ret |= EV_ACT;
510        }
511        ret
512    }
513
514    // cp_keys(ev, step, two_d) -> (dx, dy) the arrows ask for while the picker part has the
515    // focus (Shift: 10 steps; Home / End: the ends of the x axis); one axis: Up / Right add
516    fn cp_keys(&mut self, ev: u32, step: i32, two_d: bool) -> (i32, i32) {
517        if ev & EV_FOCUS == 0 {
518            return (0, 0);
519        }
520        let s = if self.mods & MOD_SHIFT != 0 { step * 10 } else { step };
521        let mut h = (self.key(VK_RIGHT) as i32 - self.key(VK_LEFT) as i32) * s;
522        if self.rtl {
523            h = -h; // the parts keep their colours left to right: arrows as drawn
524        }
525        let up = (self.key(VK_UP) as i32 - self.key(VK_DOWN) as i32) * s;
526        let (mut dx, dy) = if two_d { (h, -up) } else { (h + up, 0) };
527        if self.key(VK_HOME) {
528            dx = -100_000;
529        }
530        if self.key(VK_END) {
531            dx = 100_000;
532        }
533        if dx | dy != 0 {
534            self.wp.ctx.focus_visible = true;
535        }
536        (dx, dy)
537    }
538
539    // cp_part_end(ev, id, label, value, max): the focus ring and the record of a picker part
540    fn cp_part_end(&mut self, ev: u32, id: u32, label: &str, value: i32, max: i32) {
541        self.focus_ring_draw(ev);
542        let r = self.sem(K_SLIDER, id, Some(label), value);
543        r.max = max;
544    }
545
546    // the spectrum drawing of a hue (one shared object while the hue stays)
547    fn spectrum_drawing(&mut self, hue: u32) -> Rc<dyn Draw> {
548        let s = &mut self.m.color;
549        match &s.spec {
550            Some(d) if d.hue == hue => d.clone(),
551            _ => {
552                let d = Rc::new(SpectrumDraw { hue });
553                s.spec = Some(d.clone());
554                d
555            }
556        }
557    }
558
559    fn hue_drawing(&mut self) -> Rc<dyn Draw> {
560        self.m.color.hue.get_or_insert_with(|| Rc::new(HueDraw)).clone()
561    }
562
563    // cp_fields(id, &argb, flags) -> EV: number boxes R G B (A) / H S V and a hex text box
564    // under the picker; their values live in the field store of the picker
565    fn cp_fields(&mut self, id: u32, argb: &mut u32, flags: u32) -> u32 {
566        let mut ev = 0;
567        // this frame's values
568        let (h, s, v) = self.cp_hsv(id);
569        {
570            let f = self.m.color.fields.entry(id).or_default();
571            let c = *argb;
572            f.rgba = [((c >> 16) & 255) as i32, ((c >> 8) & 255) as i32, (c & 255) as i32, (c >> 24) as i32];
573            let mut hh = h.wrapping_add(5) / 10;
574            if hh >= 360 {
575                hh = 0;
576            }
577            f.hsv = [hh as i32, (s.wrapping_add(5) / 10) as i32, (v.wrapping_add(5) / 10) as i32];
578        }
579        // hex text, unless it is being edited
580        if self.wp.ctx.focus != part_id(id, 10) {
581            self.cf_hex_text(id, *argb, flags);
582        }
583        self.box_begin(0, BOX_V, 4);
584        for row in 0..4usize {
585            self.box_begin(0, BOX_H | BOX_XCENTER, 8);
586            // left: R, G, B, A
587            if row == 3 && flags & CP_ALPHA == 0 {
588                self.spacer(24 + 8 + 72);
589            } else {
590                self.next_size(24, 0);
591                self.text(TS_BODY, FLD_LEFT[row]);
592                self.next_size(72, 0);
593                let mut val = self.m.color.fields.get(&id).map_or(0, |f| f.rgba[row]);
594                let e = self.number_box(part_id(id, row as u32 + 3), &mut val, &RG_BYTE);
595                if let Some(f) = self.m.color.fields.get_mut(&id) {
596                    f.rgba[row] = val;
597                }
598                ev |= e;
599                if e & EV_ACT != 0 {
600                    self.cf_apply_rgba(id, argb, flags);
601                }
602            }
603            self.spacer(8);
604            self.next_size(32, 0);
605            self.text(TS_BODY, FLD_RIGHT[row]);
606            if row == 3 {
607                self.next_size(88, 0);
608                let mut t = self.m.color.fields.get(&id).map_or(String::new(), |f| f.hex.clone());
609                let e = self.textbox(part_id(id, 10), None, &mut t, 12);
610                if let Some(f) = self.m.color.fields.get_mut(&id) {
611                    f.hex = t;
612                }
613                ev |= e & !EV_ACT;
614                if e & EV_COMMIT != 0 {
615                    self.cf_apply_hex(id, argb, flags);
616                    self.cf_hex_text(id, *argb, flags); // the committed text shows the colour
617                    self.repass();
618                }
619            } else {
620                self.next_size(72, 0);
621                let mut val = self.m.color.fields.get(&id).map_or(0, |f| f.hsv[row]);
622                let rg = if row == 0 { &RG_HUE } else { &RG_PCT };
623                let e = self.number_box(part_id(id, row as u32 + 7), &mut val, rg);
624                if let Some(f) = self.m.color.fields.get_mut(&id) {
625                    f.hsv[row] = val;
626                }
627                ev |= e;
628                if e & EV_ACT != 0 {
629                    self.cf_apply_hsv(id, argb);
630                }
631            }
632            self.box_end();
633        }
634        self.box_end();
635        ev
636    }
637
638    // .cf_hex_text: the colour as RRGGBB (AARRGGBB with CP_ALPHA) into the hex field
639    fn cf_hex_text(&mut self, id: u32, argb: u32, flags: u32) {
640        let t = if flags & CP_ALPHA != 0 { format!("{:08X}", argb) } else { format!("{:06X}", argb & 0xFF_FFFF) };
641        self.m.color.fields.entry(id).or_default().hex = t;
642    }
643
644    // .cf_apply_rgba: R G B A fields -> *argb, HSV follows
645    fn cf_apply_rgba(&mut self, id: u32, argb: &mut u32, flags: u32) {
646        let f = self.m.color.fields.entry(id).or_default().rgba;
647        let mut c = (f[3] as u32) << 24 | (f[0] as u32) << 16 | (f[1] as u32) << 8 | f[2] as u32;
648        if flags & CP_ALPHA == 0 {
649            c = (c & 0xFF_FFFF) | (*argb & 0xFF00_0000);
650        }
651        self.cf_set_argb(id, argb, c);
652    }
653
654    // .cf_apply_hsv: H S V fields -> slot HSV -> *argb (alpha kept)
655    fn cf_apply_hsv(&mut self, id: u32, argb: &mut u32) {
656        let f = self.m.color.fields.entry(id).or_default().hsv;
657        let sl = self.slot(id);
658        sl.scrollx = f[0].wrapping_mul(10);
659        sl.scrolly = f[1].wrapping_mul(10);
660        sl.aux = f[2].wrapping_mul(10);
661        let (h, s, v) = (sl.scrollx as u32, sl.scrolly as u32, sl.aux as u32);
662        let c = (hsv_to_rgb(h, s, v) & 0xFF_FFFF) | (*argb & 0xFF00_0000);
663        *argb = c;
664        self.slot(id).measw = c as i32;
665        self.repass();
666    }
667
668    // .cf_apply_hex: the hex text -> *argb (6 digits keep the alpha)
669    fn cf_apply_hex(&mut self, id: u32, argb: &mut u32, _flags: u32) {
670        let t = self.m.color.fields.entry(id).or_default().hex.clone();
671        let b = t.as_bytes();
672        // hex digits (the asm tests c | 32 against '0'..'9' and 'a'..'f')
673        let n = b.iter().take_while(|&&c| c != 0).filter(|&&c| matches!(c | 32, b'0'..=b'9' | b'a'..=b'f')).count();
674        if n == 0 {
675            return;
676        }
677        let mut i = 0;
678        while i < b.len() && b[i] == b' ' {
679            i += 1;
680        }
681        let (mut c, _) = super::runtime::parse_hex(&b[i..]);
682        if n <= 6 {
683            c = (c & 0xFF_FFFF) | (*argb & 0xFF00_0000);
684        }
685        self.cf_set_argb(id, argb, c);
686    }
687
688    // .cf_set_argb(c): *argb = c, the slot's HSV follows (greys keep their hue)
689    fn cf_set_argb(&mut self, id: u32, argb: &mut u32, c: u32) {
690        if c == *argb {
691            return;
692        }
693        *argb = c;
694        let (h, s, v) = rgb_to_hsv(c);
695        let sl = self.slot(id);
696        sl.measw = c as i32;
697        if s != 0 {
698            sl.scrollx = h as i32;
699        }
700        sl.scrolly = s as i32;
701        sl.aux = v as i32;
702        self.repass();
703    }
704
705    // ------------------------------------------------------------------ colour wheel
706
707    // wc_get(id, side, v) -> the display image of wheel id at this size and value
708    fn wc_get(&mut self, id: u32, side: i32, v: i32) -> Rc<Surface> {
709        let frame = self.frame;
710        let s = &mut self.m.color;
711        // the entry of id, else a free or the least recently used one
712        let mut cand = 0;
713        let mut age = u32::MAX;
714        let mut found = None;
715        for (i, e) in s.wc.iter().enumerate() {
716            if e.id == id {
717                found = Some(i);
718                break;
719            }
720            let a = if e.id == 0 { 0 } else { e.seen };
721            if a < age {
722                age = a;
723                cand = i;
724            }
725        }
726        let i = match found {
727            Some(i) => i,
728            None => {
729                let e = &mut s.wc[cand];
730                *e = Wc { id, ..Wc::default() };
731                cand
732            }
733        };
734        let e = &mut s.wc[i];
735        e.seen = frame;
736        if e.size != side {
737            // new size: both images, the base rendered at full value
738            e.base = Surface::new(side, side, FMT_PARGB);
739            e.size = side;
740            e.v = -1;
741            wc_render_base(&mut e.base, side);
742        }
743        if e.v != v {
744            e.v = v;
745            e.disp = Rc::new(wc_render_disp(&e.base, side, v as u32));
746            s.wc_ver = s.wc_ver.wrapping_add(1);
747            e.ver = s.wc_ver;
748        }
749        s.wc[i].disp.clone()
750    }
751
752    /// ui_color_wheel(id, &mut color, flags) -> EV_*: color = [argb], or [h, s, v] with
753    /// CW_HSV
754    #[cfg_attr(jfgui_dev, track_caller)]
755    pub fn color_wheel(&mut self, id: u32, color: &mut [u32], flags: u32) -> u32 {
756        let _src = self.src_scope();
757        let hsv = flags & CW_HSV != 0;
758        let mut keych = false;
759        // the colour as hue / saturation / value
760        let (mut h, mut s, v);
761        if hsv {
762            h = color[0] as i32;
763            s = color[1] as i32;
764            v = color[2] as i32;
765        } else {
766            let sl = *self.slot(id);
767            let valid = sl.afrom == 1 && sl.ato == 0; // state valid
768            if color[0] as i32 == sl.measw && valid {
769                h = sl.scrollx;
770                s = sl.scrolly;
771                v = sl.aux;
772            } else {
773                let (hh, ss, vv) = rgb_to_hsv(color[0]);
774                h = hh as i32;
775                if ss == 0 && valid {
776                    h = sl.scrollx; // grey: keep the previous hue
777                }
778                s = ss as i32;
779                v = vv as i32;
780            }
781        }
782        let (oh, os) = (h, s);
783        let stflags = self.take_flags();
784        let n = self.dp(WHEEL_NAT);
785        self.place(n, n);
786        let mut hb = HB_FOCUS | CUR_CROSS << HB_CURSOR_SHIFT;
787        if stflags & ST_DISABLED != 0 {
788            hb |= HB_DISABLED;
789        }
790        let mut ev = self.behave(id, hb);
791        // geometry: the largest centred square
792        let r = self.wp.ctx.last_rect;
793        self.ltr_begin(r[0], r[2]); // right-to-left: hues keep their places
794        let side = r[2].min(r[3]).max(8);
795        let wx = (r[2].wrapping_sub(side) >> 1).wrapping_add(r[0]);
796        let wy = (r[3].wrapping_sub(side) >> 1).wrapping_add(r[1]);
797        let rad = ((side as u32 >> 1) as i32).wrapping_sub(1);
798        if ev & EV_HELD != 0 {
799            // pointer: hue from the angle, saturation from the distance
800            let half = (side as u32 >> 1) as i32;
801            let dx = self.ltr_x(self.wp.ctx.mx).wrapping_sub(wx).wrapping_sub(half); // px from the centre
802            let dy = wy.wrapping_add(half).wrapping_sub(self.wp.ctx.my); // up
803            let fy = dy as f64;
804            let fx = dx as f64;
805            if dx | dy != 0 {
806                // the centre: hue unchanged
807                let mut hh = f_round(deg_atan2(fy, fx) * 10.0) as i32;
808                if hh < 0 {
809                    hh += 3600;
810                }
811                if hh as u32 >= 3600 {
812                    hh -= 3600;
813                }
814                h = hh;
815            }
816            let d = sqrtd(fy * fy + fx * fx) / rad as f64;
817            s = f_round(minsd(d, 1.0) * 1000.0) as i32;
818        } else if ev & EV_FOCUS != 0 {
819            let st = if self.mods & MOD_SHIFT != 0 { 100 } else { 10 }; // 1 degree / 1 %
820            let mut dh = self.key(VK_RIGHT) as i32;
821            dh -= self.key(VK_LEFT) as i32;
822            let mut ds = self.key(VK_UP) as i32;
823            ds -= self.key(VK_DOWN) as i32;
824            if dh | ds != 0 {
825                keych = true;
826                self.wp.ctx.focus_visible = true;
827                let mut rem = (h.wrapping_add(dh.wrapping_mul(st)).wrapping_add(3600)) % 3600; // wrap 0 .. 3599
828                if rem < 0 {
829                    rem += 3600;
830                }
831                h = rem;
832                s = s.wrapping_add(ds.wrapping_mul(st)).clamp(0, 1000);
833            }
834        }
835        if h != oh || s != os {
836            ev |= EV_ACT;
837            if keych {
838                ev |= EV_COMMIT;
839            }
840            self.repass();
841            if hsv {
842                color[0] = h as u32;
843                color[1] = s as u32;
844            } else {
845                let c = hsv_to_rgb(h as u32, s as u32, v as u32) & 0xFF_FFFF;
846                color[0] = c | (color[0] & 0xFF00_0000);
847            }
848        }
849        // remember the HSV state of an ARGB colour
850        if !hsv {
851            let c = color[0];
852            let sl = self.slot(id);
853            sl.measw = c as i32;
854            sl.afrom = 1;
855            sl.ato = 0;
856            sl.scrollx = h;
857            sl.scrolly = s;
858            sl.aux = v;
859        }
860        // the cached image at the current value
861        let img = self.wc_get(id, side, v.clamp(0, 1000));
862        let ver = self.m.color.wc.iter().find(|e| e.id == id).map_or(0, |e| e.ver);
863        self.draw_image(&img, ver, wx, wy, 0, 0);
864        // paint (style): rim, marker, focus visual
865        let st = self.style;
866        let mut sp = Sp { x: wx, y: wy, w: side, h: side, value: h, value2: s, ..Sp::default() };
867        sp.s[0] = rad;
868        sp.s[1] = self.focus_ring(ev) as i32;
869        st.color_wheel(self, &sp);
870        self.ltr_end();
871        self.sem(K_SLIDER, id, None, h).max = s;
872        ev
873    }
874}