Skip to main content

teksilo_widgets/styles/
recipe_color_picker_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `ColorPickerStyle` impl + the picker's design tokens.
5//!
6//! Design tokens for `ColorPicker` live as `pub const`s on this module.
7//! The four functional renderers (`HsvCanvas`, `HueStrip`, `AlphaStrip`,
8//! `ColorSwatch`) read these directly when they need dimensions for
9//! their colour-space rendering.
10//!
11//! `RecipeColorPickerStyle::make_body` ports the IntUI layout exactly:
12//! one of three column / row arrangements per `ColorPickerLayout`,
13//! wrapped in a `Panel` surface (1 dp border + corner radius +
14//! `PADDING` interior).
15
16use teksilo_core::build_context::BuildContext;
17use teksilo_core::styles::{ColorPickerLayout, ColorPickerStyle, ColorPickerStyleConfig};
18use teksilo_core::widget_id::WidgetId;
19use teksilo_tokens::Color;
20
21use crate::panel::Panel;
22use crate::primitives::{HStack, VStack};
23
24// ─── IntUI design tokens for ColorPicker ───────────────────────────
25
26// HSV (saturation × value) canvas dimensions.
27pub const CANVAS_WIDTH: f32 = 224.0;
28pub const CANVAS_HEIGHT: f32 = 192.0;
29pub const CANVAS_CORNER_RADIUS: f32 = 4.0;
30
31// 1D hue and alpha strips (vertical orientation in default layouts).
32pub const STRIP_THICKNESS: f32 = 14.0;
33pub const STRIP_LENGTH: f32 = 192.0;
34pub const STRIP_CORNER_RADIUS: f32 = 4.0;
35
36// HSV-canvas indicator (double-ring: white outer, dark inner).
37pub const INDICATOR_RADIUS: f32 = 7.0;
38pub const INDICATOR_OUTER_STROKE_WIDTH: f32 = 1.5;
39pub const INDICATOR_INNER_STROKE_WIDTH: f32 = 1.0;
40pub const INDICATOR_OUTER_COLOR: Color = Color::WHITE;
41pub const INDICATOR_INNER_COLOR: Color = Color::new(0.0, 0.0, 0.0, 0.6);
42
43// Strip thumb (the bar across hue / alpha sliders).
44pub const STRIP_THUMB_WIDTH: f32 = 18.0;
45pub const STRIP_THUMB_HEIGHT: f32 = 8.0;
46pub const STRIP_THUMB_CORNER_RADIUS: f32 = 2.0;
47
48// Outer padding + inter-section gap.
49pub const PADDING: f32 = 12.0;
50pub const GAP: f32 = 10.0;
51
52// Preset swatch cells.
53pub const SWATCH_SIZE: f32 = 22.0;
54pub const SWATCH_SPACING: f32 = 6.0;
55pub const SWATCH_CORNER_RADIUS: f32 = 4.0;
56pub const SWATCH_SELECTED_STROKE_WIDTH: f32 = 2.0;
57
58// Checkerboard pattern (alpha visualization on swatches and the
59// alpha strip background).
60pub const CHECKER_CELL: f32 = 6.0;
61pub const CHECKER_COLOR_A: Color = Color::new(0.78, 0.78, 0.78, 1.0);
62pub const CHECKER_COLOR_B: Color = Color::WHITE;
63
64// Current-color preview swatch (inside the picker, distinct from the
65// individual ColorSwatch sizes).
66pub const PREVIEW_WIDTH: f32 = 64.0;
67pub const PREVIEW_HEIGHT: f32 = 28.0;
68pub const PREVIEW_CORNER_RADIUS: f32 = 4.0;
69
70// RGB / HSV spinner cell width and hex-input field width.
71pub const SPINNER_FIELD_WIDTH: f32 = 56.0;
72pub const HEX_FIELD_WIDTH: f32 = 96.0;
73
74/// Configurable dimension recipe for `RecipeColorPickerStyle`.
75///
76/// All fields default to the module-level `pub const` tokens so existing
77/// callers that use `RecipeColorPickerStyle::default()` are unaffected.
78/// Override individual fields to create a custom-sized picker without
79/// writing a full `ColorPickerStyle` implementation.
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct ColorPickerRecipe {
82    // HSV canvas
83    pub canvas_width: f32,
84    pub canvas_height: f32,
85    pub canvas_corner_radius: f32,
86    // 1D strips
87    pub strip_thickness: f32,
88    pub strip_length: f32,
89    pub strip_corner_radius: f32,
90    // HSV-canvas indicator
91    pub indicator_radius: f32,
92    pub indicator_outer_stroke_width: f32,
93    pub indicator_inner_stroke_width: f32,
94    pub indicator_outer_color: Color,
95    pub indicator_inner_color: Color,
96    // Strip thumb
97    pub strip_thumb_width: f32,
98    pub strip_thumb_height: f32,
99    pub strip_thumb_corner_radius: f32,
100    // Layout spacing
101    pub padding: f32,
102    pub gap: f32,
103    // Preset swatches
104    pub swatch_size: f32,
105    pub swatch_spacing: f32,
106    pub swatch_corner_radius: f32,
107    pub swatch_selected_stroke_width: f32,
108    // Checkerboard (alpha visualisation)
109    pub checker_cell: f32,
110    pub checker_color_a: Color,
111    pub checker_color_b: Color,
112    // Current-color preview swatch
113    pub preview_width: f32,
114    pub preview_height: f32,
115    pub preview_corner_radius: f32,
116    // Input fields
117    pub spinner_field_width: f32,
118    pub hex_field_width: f32,
119}
120
121impl Default for ColorPickerRecipe {
122    fn default() -> Self {
123        Self {
124            canvas_width: CANVAS_WIDTH,
125            canvas_height: CANVAS_HEIGHT,
126            canvas_corner_radius: CANVAS_CORNER_RADIUS,
127            strip_thickness: STRIP_THICKNESS,
128            strip_length: STRIP_LENGTH,
129            strip_corner_radius: STRIP_CORNER_RADIUS,
130            indicator_radius: INDICATOR_RADIUS,
131            indicator_outer_stroke_width: INDICATOR_OUTER_STROKE_WIDTH,
132            indicator_inner_stroke_width: INDICATOR_INNER_STROKE_WIDTH,
133            indicator_outer_color: INDICATOR_OUTER_COLOR,
134            indicator_inner_color: INDICATOR_INNER_COLOR,
135            strip_thumb_width: STRIP_THUMB_WIDTH,
136            strip_thumb_height: STRIP_THUMB_HEIGHT,
137            strip_thumb_corner_radius: STRIP_THUMB_CORNER_RADIUS,
138            padding: PADDING,
139            gap: GAP,
140            swatch_size: SWATCH_SIZE,
141            swatch_spacing: SWATCH_SPACING,
142            swatch_corner_radius: SWATCH_CORNER_RADIUS,
143            swatch_selected_stroke_width: SWATCH_SELECTED_STROKE_WIDTH,
144            checker_cell: CHECKER_CELL,
145            checker_color_a: CHECKER_COLOR_A,
146            checker_color_b: CHECKER_COLOR_B,
147            preview_width: PREVIEW_WIDTH,
148            preview_height: PREVIEW_HEIGHT,
149            preview_corner_radius: PREVIEW_CORNER_RADIUS,
150            spinner_field_width: SPINNER_FIELD_WIDTH,
151            hex_field_width: HEX_FIELD_WIDTH,
152        }
153    }
154}
155
156/// Default `ColorPickerStyle` shipped with Teksilo.
157#[derive(Debug, Default, Clone, Copy)]
158pub struct RecipeColorPickerStyle {
159    pub recipe: ColorPickerRecipe,
160}
161
162impl RecipeColorPickerStyle {
163    pub fn new(recipe: ColorPickerRecipe) -> Self {
164        Self { recipe }
165    }
166}
167
168impl ColorPickerStyle for RecipeColorPickerStyle {
169    fn make_body(&self, cfg: &ColorPickerStyleConfig, ctx: &mut BuildContext) -> WidgetId {
170        let body = match cfg.layout {
171            ColorPickerLayout::Compact => {
172                let mut col = VStack::new()
173                    .spacing(self.recipe.gap)
174                    .add_child(cfg.top_row);
175                if let Some(hex) = cfg.compact_hex {
176                    col = col.add_child(hex);
177                }
178                if let Some(footer) = cfg.footer {
179                    col = col.add_child(footer);
180                }
181                ctx.add(col)
182            }
183            ColorPickerLayout::Standard => {
184                let mut col = VStack::new()
185                    .spacing(self.recipe.gap)
186                    .add_child(cfg.top_row);
187                if let Some(preview) = cfg.preview_row {
188                    col = col.add_child(preview);
189                }
190                if let Some(rgb) = cfg.rgb_row {
191                    col = col.add_child(rgb);
192                }
193                if let Some(hsv) = cfg.hsv_row {
194                    col = col.add_child(hsv);
195                }
196                if let Some(grid) = cfg.swatches {
197                    col = col.add_child(grid);
198                }
199                if let Some(footer) = cfg.footer {
200                    col = col.add_child(footer);
201                }
202                ctx.add(col)
203            }
204            ColorPickerLayout::Wide => {
205                let mut side_col = VStack::new().spacing(self.recipe.gap);
206                if let Some(preview) = cfg.preview_row {
207                    side_col = side_col.add_child(preview);
208                }
209                if let Some(rgb) = cfg.rgb_row {
210                    side_col = side_col.add_child(rgb);
211                }
212                if let Some(hsv) = cfg.hsv_row {
213                    side_col = side_col.add_child(hsv);
214                }
215                let side_col_id = ctx.add(side_col);
216                let main_row_id = ctx.add(
217                    HStack::new()
218                        .spacing(self.recipe.gap)
219                        .add_child(cfg.top_row)
220                        .add_child(side_col_id),
221                );
222                let mut col = VStack::new()
223                    .spacing(self.recipe.gap)
224                    .add_child(main_row_id);
225                if let Some(grid) = cfg.swatches {
226                    col = col.add_child(grid);
227                }
228                if let Some(footer) = cfg.footer {
229                    col = col.add_child(footer);
230                }
231                ctx.add(col)
232            }
233        };
234
235        // Wrap in a Panel so the picker reads as a self-contained
236        // surface (background + border + corner radius). Embedding in
237        // an overlay without this produces a transparent popup.
238        ctx.add(
239            Panel::new()
240                .padding(self.recipe.padding)
241                .border_width(1.0)
242                .child_id(body),
243        )
244    }
245}