Skip to main content

teksilo_widgets/color_picker/
state.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Internal state for [`ColorPicker`](super::ColorPicker).
5//!
6//! `ColorComponents` is constructed once in the picker's `build()` and
7//! holds derived `Signal`s for each RGB / HSV channel plus typed
8//! setters that recompose the bound `Signal<Color>` when a single
9//! channel changes. Centralizing the conversions here keeps every
10//! subwidget (HSV canvas, hue strip, alpha strip, RGB/HSV spinners,
11//! preview) reading from clean per-channel signals without duplicating
12//! the conversion logic.
13//!
14//! # Hue preservation across grays
15//!
16//! When saturation drops to 0 or value drops to 0, the underlying
17//! sRGB representation has no hue (gray / black). A naive
18//! `value.map(|c| c.to_hsv().0)` would clamp the visible hue back to
19//! 0° — which makes the HSV canvas snap from "red" back to "red at the
20//! top-left" the moment the user drags down to white. We avoid this by
21//! caching the last non-degenerate hue in `ColorComponents` and
22//! returning it whenever the bound color's saturation/value collapses.
23
24use std::cell::Cell;
25use std::rc::Rc;
26
27use teksilo_core::build_context::BuildContext;
28use teksilo_core::signal::Signal;
29use teksilo_tokens::Color;
30
31/// Derived signals + setters for the RGB and HSV channels of a bound
32/// `Signal<Color>`.
33#[allow(dead_code)]
34pub(crate) struct ColorComponents {
35    pub value: Signal<Color>,
36
37    // ── RGB ── (each in 0..=1)
38    pub red: Signal<f32>,
39    pub green: Signal<f32>,
40    pub blue: Signal<f32>,
41    pub alpha: Signal<f32>,
42
43    // ── HSV ──
44    pub hue: Signal<f32>,        // 0..360
45    pub saturation: Signal<f32>, // 0..1
46    pub value_hsv: Signal<f32>,  // 0..1
47
48    // ── Setters ──
49    pub set_red: Rc<dyn Fn(f32)>,
50    pub set_green: Rc<dyn Fn(f32)>,
51    pub set_blue: Rc<dyn Fn(f32)>,
52    pub set_alpha: Rc<dyn Fn(f32)>,
53    pub set_hue: Rc<dyn Fn(f32)>,
54    pub set_saturation: Rc<dyn Fn(f32)>,
55    pub set_value_hsv: Rc<dyn Fn(f32)>,
56    /// Batch-update the entire HSV triple in one signal write — used
57    /// by the HSV canvas drag handler so the bound signal mutates
58    /// once per pointer event instead of three times.
59    pub set_hsv: Rc<dyn Fn(f32, f32, f32)>,
60
61    /// Shared mid-drag flag — set by HSV canvas, hue strip, and alpha
62    /// strip during a pointer drag, used by the picker's
63    /// HexColorInput-feedback effect to skip non-focused reformats.
64    /// Mirrors the Slider pattern.
65    pub dragging: Rc<Cell<bool>>,
66
67    /// Last hue with non-zero saturation. Returned by `hue` when the
68    /// current color is a gray (saturation = 0) so the HSV canvas
69    /// keeps its base color as the user drags through white / black.
70    last_hue_cell: Rc<Cell<f32>>,
71}
72
73impl ColorComponents {
74    pub(crate) fn new(ctx: &mut BuildContext, value: Signal<Color>) -> Self {
75        let initial = value.get();
76        let (init_h, init_s, _init_v) = initial.to_hsv();
77        let last_hue_cell = Rc::new(Cell::new(if init_s > 1e-6 { init_h } else { 0.0 }));
78
79        // Update last_hue_cell whenever the bound color has a real hue.
80        {
81            let cell = last_hue_cell.clone();
82            ctx.effect(&value, move |c| {
83                let (h, s, _v) = c.to_hsv();
84                if s > 1e-6 {
85                    cell.set(h);
86                }
87            });
88        }
89
90        // RGB derived signals.
91        let red = value.map(|c| c.r());
92        let green = value.map(|c| c.g());
93        let blue = value.map(|c| c.b());
94        let alpha = value.map(|c| c.a());
95
96        // HSV derived signals — saturation/value are direct; hue
97        // substitutes the cached last-hue when current is degenerate.
98        let hue = {
99            let cell = last_hue_cell.clone();
100            value.map(move |c| {
101                let (h, s, _v) = c.to_hsv();
102                if s > 1e-6 { h } else { cell.get() }
103            })
104        };
105        let saturation = value.map(|c| c.to_hsv().1);
106        let value_hsv = value.map(|c| c.to_hsv().2);
107
108        // Setters — each writes a re-composed Color back to the bound signal.
109        let set_red = {
110            let v = value.clone();
111            Rc::new(move |r: f32| {
112                let c = v.get();
113                v.set(Color::from_rgba(r.clamp(0.0, 1.0), c.g(), c.b(), c.a()));
114            }) as Rc<dyn Fn(f32)>
115        };
116        let set_green = {
117            let v = value.clone();
118            Rc::new(move |g: f32| {
119                let c = v.get();
120                v.set(Color::from_rgba(c.r(), g.clamp(0.0, 1.0), c.b(), c.a()));
121            }) as Rc<dyn Fn(f32)>
122        };
123        let set_blue = {
124            let v = value.clone();
125            Rc::new(move |b: f32| {
126                let c = v.get();
127                v.set(Color::from_rgba(c.r(), c.g(), b.clamp(0.0, 1.0), c.a()));
128            }) as Rc<dyn Fn(f32)>
129        };
130        let set_alpha = {
131            let v = value.clone();
132            Rc::new(move |a: f32| {
133                let c = v.get();
134                v.set(Color::from_rgba(c.r(), c.g(), c.b(), a.clamp(0.0, 1.0)));
135            }) as Rc<dyn Fn(f32)>
136        };
137        let set_hue = {
138            let v = value.clone();
139            let cell = last_hue_cell.clone();
140            Rc::new(move |h: f32| {
141                let c = v.get();
142                let (_old_h, s, val) = c.to_hsv();
143                let h_norm = h.rem_euclid(360.0);
144                cell.set(h_norm);
145                v.set(Color::from_hsva(h_norm, s, val, c.a()));
146            }) as Rc<dyn Fn(f32)>
147        };
148        let set_saturation = {
149            let v = value.clone();
150            let cell = last_hue_cell.clone();
151            Rc::new(move |s: f32| {
152                let c = v.get();
153                let (cur_h, _s, val) = c.to_hsv();
154                let h = if c.to_hsv().1 > 1e-6 {
155                    cur_h
156                } else {
157                    cell.get()
158                };
159                v.set(Color::from_hsva(h, s.clamp(0.0, 1.0), val, c.a()));
160            }) as Rc<dyn Fn(f32)>
161        };
162        let set_value_hsv = {
163            let v = value.clone();
164            let cell = last_hue_cell.clone();
165            Rc::new(move |val: f32| {
166                let c = v.get();
167                let (cur_h, s, _v) = c.to_hsv();
168                let h = if s > 1e-6 { cur_h } else { cell.get() };
169                v.set(Color::from_hsva(h, s, val.clamp(0.0, 1.0), c.a()));
170            }) as Rc<dyn Fn(f32)>
171        };
172        let set_hsv = {
173            let v = value.clone();
174            let cell = last_hue_cell.clone();
175            Rc::new(move |h: f32, s: f32, val: f32| {
176                let c = v.get();
177                let h_norm = h.rem_euclid(360.0);
178                if s > 1e-6 {
179                    cell.set(h_norm);
180                }
181                v.set(Color::from_hsva(
182                    h_norm,
183                    s.clamp(0.0, 1.0),
184                    val.clamp(0.0, 1.0),
185                    c.a(),
186                ));
187            }) as Rc<dyn Fn(f32, f32, f32)>
188        };
189
190        Self {
191            value,
192            red,
193            green,
194            blue,
195            alpha,
196            hue,
197            saturation,
198            value_hsv,
199            set_red,
200            set_green,
201            set_blue,
202            set_alpha,
203            set_hue,
204            set_saturation,
205            set_value_hsv,
206            set_hsv,
207            dragging: Rc::new(Cell::new(false)),
208            last_hue_cell,
209        }
210    }
211}