Skip to main content

ccf_gpui_widgets/widgets/
color_swatch.rs

1//! Color swatch widget
2//!
3//! A color preview with hex input and color picker. Displays a colored square alongside
4//! a text input for the hex color value. Clicking the swatch opens a full color picker
5//! with RGB/HSL sliders and a 2D saturation/lightness selector.
6//!
7//! # Features
8//!
9//! - Hex color input (#RGB, #RRGGBB, #RRGGBBAA)
10//! - CSS named color support (140 colors: "red", "coral", "darkblue", etc.)
11//! - Color picker popup with RGB and HSL modes
12//! - 2D saturation/lightness selector canvas
13//! - Hue rainbow slider
14//! - Optional alpha channel support
15//! - Old/new color preview
16//!
17//! # Example
18//!
19//! ```ignore
20//! use ccf_gpui_widgets::widgets::ColorSwatch;
21//!
22//! let swatch = cx.new(|cx| {
23//!     ColorSwatch::new(cx)
24//!         .value("#3b82f6")
25//!         .with_alpha(true)
26//! });
27//!
28//! // Subscribe to changes
29//! cx.subscribe(&swatch, |this, _swatch, event: &ColorSwatchEvent, cx| {
30//!     if let ColorSwatchEvent::Change(hex) = event {
31//!         println!("Color: {}", hex);
32//!     }
33//! }).detach();
34//! ```
35
36use std::cell::Cell;
37use std::rc::Rc;
38
39use gpui::prelude::*;
40use gpui::*;
41
42use crate::theme::{get_theme_or, Theme};
43use crate::utils::color::{Rgb, Hsl, Hsv, parse_color, parse_color_alpha};
44use super::text_input::{TextInput, TextInputEvent};
45use super::focus_navigation::{FocusNext, FocusPrev};
46use super::button::{primary_button, secondary_button};
47
48// Actions for keyboard navigation
49actions!(ccf_color_swatch, [ClosePicker, ApplyPicker]);
50
51/// Register key bindings for color swatch components
52///
53/// Call this once at application startup:
54/// ```ignore
55/// ccf_gpui_widgets::widgets::color_swatch::register_keybindings(cx);
56/// ```
57pub fn register_keybindings(cx: &mut App) {
58    cx.bind_keys([
59        KeyBinding::new("escape", ClosePicker, Some("CcfColorPicker")),
60        KeyBinding::new("enter", ApplyPicker, Some("CcfColorPicker")),
61    ]);
62}
63
64/// Drag state for saturation/lightness canvas
65#[doc(hidden)]
66#[derive(Clone)]
67struct SlDrag {
68    canvas_origin: Rc<Cell<Point<Pixels>>>,
69    canvas_width: f32,
70    canvas_height: f32,
71}
72
73/// Drag state for hue slider
74#[doc(hidden)]
75#[derive(Clone)]
76struct HueDrag {
77    origin: Rc<Cell<f32>>,
78    width: Rc<Cell<f32>>,
79}
80
81/// Drag state for alpha slider
82#[doc(hidden)]
83#[derive(Clone)]
84struct AlphaDrag {
85    origin: Rc<Cell<f32>>,
86    width: Rc<Cell<f32>>,
87}
88
89/// Drag state for component sliders (R, G, B, S, L)
90#[doc(hidden)]
91#[derive(Clone)]
92struct ComponentDrag {
93    origin: Rc<Cell<f32>>,
94    width: Rc<Cell<f32>>,
95    handle_visual_width: f32,
96    max_value: f32,
97    update_fn: fn(&mut ColorSwatch, f32, &mut Context<ColorSwatch>),
98}
99
100/// Empty view for drag visualization (we don't need a visual)
101#[doc(hidden)]
102struct EmptyDragView;
103
104impl Render for EmptyDragView {
105    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
106        div().size_0()
107    }
108}
109
110/// Events emitted by ColorSwatch
111#[derive(Clone, Debug)]
112pub enum ColorSwatchEvent {
113    /// Color value changed
114    Change(String),
115}
116
117/// Color picker mode (RGB or HSL sliders)
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub enum PickerMode {
120    Rgb,
121    Hsl,
122}
123
124/// Color swatch widget with hex input and color picker
125pub struct ColorSwatch {
126    /// Current color value as hex (#RRGGBB or #RRGGBBAA)
127    value: String,
128    /// Placeholder text
129    placeholder: String,
130    /// Whether alpha channel is enabled
131    with_alpha: bool,
132    /// Whether the widget is enabled
133    enabled: bool,
134    /// Custom theme
135    custom_theme: Option<Theme>,
136    /// Focus handle (for focus navigation, not key capture)
137    focus_handle: FocusHandle,
138    /// Focus handle for the picker popup (for ESC key handling)
139    picker_focus_handle: FocusHandle,
140    /// Text input for hex editing
141    hex_input: Entity<TextInput>,
142    /// Whether picker popup is open
143    is_picker_open: bool,
144    /// Current RGB values
145    current_rgb: Rgb,
146    /// Current HSL values (used for H slider and conversion)
147    current_hsl: Hsl,
148    /// Current HSV values (used for the S/V canvas)
149    current_hsv: Hsv,
150    /// Current alpha value (0-255)
151    current_alpha: u8,
152    /// Original color when picker opened (for comparison)
153    original_value: String,
154    /// Whether the text input needs to be synced with value
155    needs_input_sync: bool,
156    /// Whether the current input is valid
157    input_is_valid: bool,
158    /// Measured hue slider width (persists between frames)
159    hue_slider_width: Rc<Cell<f32>>,
160    /// Measured hue slider origin (persists between frames)
161    hue_slider_origin: Rc<Cell<f32>>,
162    /// Measured alpha slider width (persists between frames)
163    alpha_slider_width: Rc<Cell<f32>>,
164    /// Measured alpha slider origin (persists between frames)
165    alpha_slider_origin: Rc<Cell<f32>>,
166}
167
168impl EventEmitter<ColorSwatchEvent> for ColorSwatch {}
169
170impl Focusable for ColorSwatch {
171    fn focus_handle(&self, _cx: &App) -> FocusHandle {
172        self.focus_handle.clone()
173    }
174}
175
176impl ColorSwatch {
177    /// Create a new color swatch
178    pub fn new(cx: &mut Context<Self>) -> Self {
179        let hex_input = cx.new(|cx| {
180            TextInput::new(cx)
181                .placeholder("#000000")
182                .with_value("#000000")
183        });
184
185        // Subscribe to text input events
186        cx.subscribe(&hex_input, |this, input, event: &TextInputEvent, cx| {
187            match event {
188                TextInputEvent::Change => {
189                    // Get the current input value and update preview
190                    let value = input.read(cx).content().to_string();
191                    this.handle_input_change(&value, cx);
192                }
193                TextInputEvent::Enter | TextInputEvent::Blur => {
194                    // Try to parse as named color or hex on Enter/Blur
195                    let value = input.read(cx).content().to_string();
196                    this.handle_input_commit(&value, cx);
197                }
198                _ => {}
199            }
200        }).detach();
201
202        Self {
203            value: "#000000".to_string(),
204            placeholder: "#000000".to_string(),
205            with_alpha: false,
206            enabled: true,
207            custom_theme: None,
208            focus_handle: cx.focus_handle(),
209            picker_focus_handle: cx.focus_handle(),
210            hex_input,
211            is_picker_open: false,
212            current_rgb: Rgb::new(0, 0, 0),
213            current_hsl: Hsl::new(0.0, 0.0, 0.0),
214            current_hsv: Hsv::new(0.0, 0.0, 0.0),
215            current_alpha: 255,
216            original_value: "#000000".to_string(),
217            needs_input_sync: false,
218            input_is_valid: true,
219            // Initial estimates, will be updated by prepaint
220            hue_slider_width: Rc::new(Cell::new(200.0)),
221            hue_slider_origin: Rc::new(Cell::new(0.0)),
222            alpha_slider_width: Rc::new(Cell::new(200.0)),
223            alpha_slider_origin: Rc::new(Cell::new(0.0)),
224        }
225    }
226
227    /// Set initial value (builder pattern)
228    /// Accepts hex colors (#RGB, #RRGGBB, #RRGGBBAA) or CSS named colors
229    #[must_use]
230    pub fn with_value(mut self, color: impl Into<String>) -> Self {
231        let color_str = color.into();
232        // Try to parse as hex or named color
233        if let Some(rgba) = parse_color_alpha(&color_str) {
234            self.current_rgb = Rgb::new(rgba.r, rgba.g, rgba.b);
235            self.current_hsl = self.current_rgb.to_hsl();
236            self.current_hsv = self.current_rgb.to_hsv();
237            self.current_alpha = rgba.a;
238            self.value = if self.with_alpha && rgba.a != 255 {
239                format!("#{:02X}{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b, rgba.a)
240            } else {
241                format!("#{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b)
242            };
243            // Flag that text input needs to be synced on first render
244            self.needs_input_sync = true;
245        }
246        self
247    }
248
249    /// Set placeholder text (builder pattern)
250    #[must_use]
251    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
252        self.placeholder = text.into();
253        self
254    }
255
256    /// Enable or disable alpha channel support (builder pattern)
257    #[must_use]
258    pub fn with_alpha(mut self, enabled: bool) -> Self {
259        self.with_alpha = enabled;
260        self
261    }
262
263    /// Set enabled state (builder pattern)
264    #[must_use]
265    pub fn with_enabled(mut self, enabled: bool) -> Self {
266        self.enabled = enabled;
267        self
268    }
269
270    /// Set custom theme (builder pattern)
271    #[must_use]
272    pub fn theme(mut self, theme: Theme) -> Self {
273        self.custom_theme = Some(theme);
274        self
275    }
276
277    /// Get the current hex value
278    pub fn value(&self) -> &str {
279        &self.value
280    }
281
282    /// Get current RGB value
283    pub fn rgb(&self) -> Rgb {
284        self.current_rgb
285    }
286
287    /// Get current HSL value
288    pub fn hsl(&self) -> Hsl {
289        self.current_hsl
290    }
291
292    /// Get current alpha value (0-255)
293    pub fn alpha(&self) -> u8 {
294        self.current_alpha
295    }
296
297    /// Check if the widget is enabled
298    pub fn is_enabled(&self) -> bool {
299        self.enabled
300    }
301
302    /// Set enabled state programmatically
303    pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
304        if self.enabled != enabled {
305            self.enabled = enabled;
306            // Sync enabled state to the hex input
307            self.hex_input.update(cx, |input, cx| {
308                input.set_enabled(enabled, cx);
309            });
310            cx.notify();
311        }
312    }
313
314    /// Set value programmatically
315    pub fn set_value(&mut self, color: &str, cx: &mut Context<Self>) {
316        self.set_value_internal(color, cx);
317        cx.emit(ColorSwatchEvent::Change(self.value.clone()));
318    }
319
320    /// Get the focus handle
321    pub fn focus_handle(&self) -> &FocusHandle {
322        &self.focus_handle
323    }
324
325    /// Internal set value without emitting event
326    fn set_value_internal(&mut self, color: &str, cx: &mut Context<Self>) {
327        // Try to parse as hex or named color
328        if let Some(rgba) = parse_color_alpha(color) {
329            self.current_rgb = Rgb::new(rgba.r, rgba.g, rgba.b);
330            self.current_hsl = self.current_rgb.to_hsl();
331            self.current_hsv = self.current_rgb.to_hsv();
332            self.current_alpha = rgba.a;
333            self.value = if self.with_alpha && rgba.a != 255 {
334                format!("#{:02X}{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b, rgba.a)
335            } else {
336                format!("#{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b)
337            };
338            // Update text input
339            self.hex_input.update(cx, |input, cx| {
340                input.set_value(&self.value, cx);
341            });
342        }
343        cx.notify();
344    }
345
346    /// Handle input text change (live preview without committing)
347    fn handle_input_change(&mut self, value: &str, cx: &mut Context<Self>) {
348        // Try to parse and update preview
349        if let Some(rgb) = parse_color(value) {
350            self.current_rgb = rgb;
351            self.current_hsl = rgb.to_hsl();
352            self.current_hsv = rgb.to_hsv();
353            // Update the value but don't update the text input (user is typing)
354            self.value = format!("#{:02X}{:02X}{:02X}", rgb.r, rgb.g, rgb.b);
355            self.input_is_valid = true;
356            cx.emit(ColorSwatchEvent::Change(self.value.clone()));
357        } else {
358            self.input_is_valid = false;
359        }
360        cx.notify();
361    }
362
363    /// Handle input commit (Enter/Blur) - parse named colors
364    fn handle_input_commit(&mut self, value: &str, cx: &mut Context<Self>) {
365        if let Some(rgba) = parse_color_alpha(value) {
366            self.current_rgb = Rgb::new(rgba.r, rgba.g, rgba.b);
367            self.current_hsl = self.current_rgb.to_hsl();
368            self.current_hsv = self.current_rgb.to_hsv();
369            self.current_alpha = rgba.a;
370            self.value = if self.with_alpha && rgba.a != 255 {
371                format!("#{:02X}{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b, rgba.a)
372            } else {
373                format!("#{:02X}{:02X}{:02X}", rgba.r, rgba.g, rgba.b)
374            };
375            self.input_is_valid = true;
376            // Update text input to show hex value (convert named colors)
377            self.hex_input.update(cx, |input, cx| {
378                input.set_value(&self.value, cx);
379            });
380            cx.emit(ColorSwatchEvent::Change(self.value.clone()));
381        } else {
382            self.input_is_valid = false;
383        }
384        cx.notify();
385    }
386
387    /// Check if the current input is valid
388    pub fn is_input_valid(&self) -> bool {
389        self.input_is_valid
390    }
391
392    /// Update from RGB values
393    fn update_from_rgb(&mut self, r: u8, g: u8, b: u8, cx: &mut Context<Self>) {
394        self.current_rgb = Rgb::new(r, g, b);
395        self.current_hsl = self.current_rgb.to_hsl();
396        self.current_hsv = self.current_rgb.to_hsv();
397        self.sync_value(cx);
398    }
399
400    /// Update from HSV values (used by the S/V canvas)
401    ///
402    /// Updates all internal color representations (HSV, RGB, HSL) while preserving
403    /// the hue value in HSL to keep the hue slider consistent.
404    fn update_from_hsv(&mut self, h: f32, s: f32, v: f32, cx: &mut Context<Self>) {
405        self.current_hsv = Hsv::new(h, s, v);
406        self.current_rgb = self.current_hsv.to_rgb();
407        self.current_hsl = self.current_rgb.to_hsl();
408        // Preserve hue in HSL to match HSV hue
409        self.current_hsl = Hsl::new(h, self.current_hsl.s, self.current_hsl.l);
410        self.sync_value(cx);
411    }
412
413    /// Sync value string and text input from current RGB
414    fn sync_value(&mut self, cx: &mut Context<Self>) {
415        let rgb = self.current_rgb;
416        self.value = if self.with_alpha && self.current_alpha != 255 {
417            format!("#{:02X}{:02X}{:02X}{:02X}", rgb.r, rgb.g, rgb.b, self.current_alpha)
418        } else {
419            format!("#{:02X}{:02X}{:02X}", rgb.r, rgb.g, rgb.b)
420        };
421        self.hex_input.update(cx, |input, cx| {
422            input.set_value(&self.value, cx);
423        });
424        cx.emit(ColorSwatchEvent::Change(self.value.clone()));
425        cx.notify();
426    }
427
428    /// Open the color picker popup
429    fn open_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
430        self.original_value = self.value.clone();
431        self.is_picker_open = true;
432        self.picker_focus_handle.focus(window);
433        cx.notify();
434    }
435
436    /// Close the color picker popup
437    fn close_picker(&mut self, cx: &mut Context<Self>) {
438        self.is_picker_open = false;
439        cx.notify();
440    }
441
442    /// Cancel and revert to original color
443    fn cancel_picker(&mut self, cx: &mut Context<Self>) {
444        // Revert to original value
445        self.set_value_internal(&self.original_value.clone(), cx);
446        self.is_picker_open = false;
447        cx.notify();
448    }
449
450    /// Apply current color and close
451    fn apply_picker(&mut self, cx: &mut Context<Self>) {
452        // Value is already set, just close
453        self.is_picker_open = false;
454        cx.notify();
455    }
456
457    /// Parse the current value to get a GPUI Rgba for display
458    fn parse_display_color(&self) -> Rgba {
459        let rgb = self.current_rgb;
460        let a = self.current_alpha;
461        rgba(((rgb.r as u32) << 24) | ((rgb.g as u32) << 16) | ((rgb.b as u32) << 8) | (a as u32))
462    }
463
464    /// Parse original value for comparison display
465    fn parse_original_color(&self) -> Rgba {
466        if let Some(rgba_val) = parse_color_alpha(&self.original_value) {
467            rgba(((rgba_val.r as u32) << 24) | ((rgba_val.g as u32) << 16) | ((rgba_val.b as u32) << 8) | (rgba_val.a as u32))
468        } else {
469            rgba(0x000000FF)
470        }
471    }
472
473    /// Handle S/V canvas interaction at position (HSV model)
474    ///
475    /// Converts mouse position to saturation/value coordinates:
476    /// - X axis = Saturation (0% left to 100% right)
477    /// - Y axis = Value/Brightness (100% top to 0% bottom)
478    fn handle_sl_at_position(&mut self, x: f32, y: f32, origin: Point<Pixels>, canvas_width: f32, canvas_height: f32, cx: &mut Context<Self>) {
479        let origin_x: f32 = origin.x.into();
480        let origin_y: f32 = origin.y.into();
481        let rel_x = (x - origin_x).clamp(0.0, canvas_width);
482        let rel_y = (y - origin_y).clamp(0.0, canvas_height);
483
484        let s = (rel_x / canvas_width) * 100.0;
485        let v = (1.0 - rel_y / canvas_height) * 100.0;
486        self.update_from_hsv(self.current_hsv.h, s, v, cx);
487    }
488
489    /// Handle hue slider interaction at position
490    ///
491    /// Converts mouse position to hue value (0-359°). Hue is clamped to prevent
492    /// wrap-around (360° = 0° = red).
493    fn handle_hue_at_position(&mut self, x: f32, origin_x: f32, slider_width: f32, cx: &mut Context<Self>) {
494        // Must match the display calculation
495        // Note: border doesn't affect layout width in GPUI, only content width matters
496        let handle_width = 4.0f32;
497        let usable_width = slider_width - handle_width;
498        // Guard against division by zero (can happen if slider hasn't been measured yet)
499        if usable_width <= 0.0 {
500            return;
501        }
502        // Map click position to handle left edge position, then to value
503        // Clicking anywhere on the slider should work, with clamping at edges
504        let rel_x = (x - origin_x - handle_width / 2.0).clamp(0.0, usable_width);
505        // Cap at 359 to prevent wrap-around to pure red (360° = 0°)
506        let h = (rel_x / usable_width) * 359.0;
507        // Use HSV for hue changes to keep S/V canvas consistent
508        self.update_from_hsv(h, self.current_hsv.s, self.current_hsv.v, cx);
509    }
510
511    /// Handle alpha slider interaction at position
512    ///
513    /// Converts mouse position to alpha value (0-255).
514    fn handle_alpha_at_position(&mut self, x: f32, origin_x: f32, slider_width: f32, cx: &mut Context<Self>) {
515        // Must match the display calculation
516        // Note: border doesn't affect layout width in GPUI, only content width matters
517        let handle_width = 4.0f32;
518        let usable_width = slider_width - handle_width;
519        // Guard against division by zero (can happen if slider hasn't been measured yet)
520        if usable_width <= 0.0 {
521            return;
522        }
523        // Map click position to handle left edge position, then to value
524        let rel_x = (x - origin_x - handle_width / 2.0).clamp(0.0, usable_width);
525        self.current_alpha = ((rel_x / usable_width) * 255.0) as u8;
526        self.sync_value(cx);
527    }
528}
529
530impl Render for ColorSwatch {
531    fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
532        // Sync text input if needed (for builder pattern with_value)
533        if self.needs_input_sync {
534            self.needs_input_sync = false;
535            let value = self.value.clone();
536            let enabled = self.enabled;
537            self.hex_input.update(cx, |input, cx| {
538                input.set_value(&value, cx);
539                input.set_enabled(enabled, cx);
540            });
541        }
542
543        let theme = get_theme_or(cx, self.custom_theme.as_ref());
544        let color = self.parse_display_color();
545        let is_picker_open = self.is_picker_open;
546        let hex_input = self.hex_input.clone();
547        let enabled = self.enabled;
548
549        let bg_popup = theme.bg_secondary;
550        let border_checkbox = theme.border_checkbox;
551        let border_input = theme.border_input;
552        let text_color = theme.text_primary;
553        let picker_focus_handle = self.picker_focus_handle.clone();
554
555        div()
556            .id("ccf_color_swatch")
557            .relative()
558            // Focus navigation (Tab / Shift+Tab) - but don't track focus, let TextInput handle it
559            .on_action(cx.listener(|this, _: &FocusNext, window, _cx| {
560                if !this.enabled {
561                    return;
562                }
563                window.focus_next();
564            }))
565            .on_action(cx.listener(|this, _: &FocusPrev, window, _cx| {
566                if !this.enabled {
567                    return;
568                }
569                window.focus_prev();
570            }))
571            .child(
572                div()
573                    .flex()
574                    .flex_row()
575                    .gap_2()
576                    .items_center()
577                    .child(
578                        // Color preview box (clickable to open picker)
579                        div()
580                            .id("ccf_color_preview")
581                            .relative()
582                            .w(px(40.))
583                            .h(px(32.))
584                            .border_1()
585                            .border_color(rgb(border_checkbox))
586                            .rounded_md()
587                            .overflow_hidden()
588                            .when(enabled, |d| d.cursor_pointer())
589                            .when(!enabled, |d| d.cursor_default().opacity(0.5))
590                            // Checkerboard background for alpha visualization
591                            .when(self.with_alpha, |d| d.child(Self::render_checkerboard()))
592                            // Color overlay
593                            .child(
594                                div()
595                                    .size_full()
596                                    .absolute()
597                                    .bg(color)
598                            )
599                            .on_click(cx.listener(|this, _event, window, cx| {
600                                if !this.enabled {
601                                    return;
602                                }
603                                if this.is_picker_open {
604                                    this.close_picker(cx);
605                                } else {
606                                    this.open_picker(window, cx);
607                                }
608                            }))
609                    )
610                    .child(
611                        // Hex color text input with error border
612                        div()
613                            .flex_1()
614                            .border_2()
615                            .rounded_md()
616                            .border_color(if self.input_is_valid {
617                                rgba(0x00000000)
618                            } else {
619                                rgb(theme.border_error)
620                            })
621                            .child(hex_input)
622                    )
623            )
624            // Color picker popup
625            .when(is_picker_open, |parent| {
626                let current_rgb = self.current_rgb;
627                let current_hsv = self.current_hsv;
628                let current_alpha = self.current_alpha;
629                let with_alpha = self.with_alpha;
630                let original_color = self.parse_original_color();
631                let new_color = self.parse_display_color();
632                let original_hex = self.original_value.clone();
633                let new_hex = self.value.clone();
634
635                // 2D S/V canvas dimensions (HSV model)
636                let canvas_width = 200.0f32;
637                let canvas_height = 150.0f32;
638                let hue = current_hsv.h;
639
640                // Canvas origin for mouse handling (shared via Rc<Cell<>>)
641                let canvas_origin = Rc::new(Cell::new(Point::default()));
642                let canvas_origin_for_paint = canvas_origin.clone();
643                let canvas_origin_for_drag = canvas_origin.clone();
644
645                // Hue slider - use persistent fields from struct
646                let hue_origin = self.hue_slider_origin.clone();
647                let hue_origin_for_paint = hue_origin.clone();
648                let hue_origin_for_drag = hue_origin.clone();
649                let hue_width = self.hue_slider_width.clone();
650                let hue_width_for_paint = hue_width.clone();
651                let hue_width_for_drag = hue_width.clone();
652
653                // Alpha slider - use persistent fields from struct
654                let alpha_origin = self.alpha_slider_origin.clone();
655                let alpha_origin_for_paint = alpha_origin.clone();
656                let alpha_origin_for_drag = alpha_origin.clone();
657                let alpha_width = self.alpha_slider_width.clone();
658                let alpha_width_for_paint = alpha_width.clone();
659                let alpha_width_for_drag = alpha_width.clone();
660
661                parent.child(
662                    deferred(
663                        anchored()
664                            .anchor(Corner::TopLeft)
665                            .child(
666                                div()
667                                    .id("ccf_color_picker")
668                                    .key_context("CcfColorPicker")
669                                    .track_focus(&picker_focus_handle)
670                                    .on_action(cx.listener(|this, _: &ClosePicker, _window, cx| {
671                                        this.cancel_picker(cx);
672                                    }))
673                                    .on_action(cx.listener(|this, _: &ApplyPicker, _window, cx| {
674                                        this.apply_picker(cx);
675                                    }))
676                                    .occlude()
677                                    .absolute()
678                                    .top(px(4.))  // Small gap below the main control
679                                    .left_0()
680                                    .w(px(280.))
681                                    .p_3()
682                                    .bg(rgb(bg_popup))
683                                    .border_1()
684                                    .border_color(rgb(border_input))
685                                    .rounded_lg()
686                                    .shadow_lg()
687                                    .flex()
688                                    .flex_col()
689                                    .gap_3()
690                                    // 2D Saturation/Lightness canvas
691                                    .child(
692                                        div()
693                                            .id("sl_canvas")
694                                            .relative()
695                                            .w(px(canvas_width))
696                                            .h(px(canvas_height))
697                                            .rounded_md()
698                                            .border_1()
699                                            .border_color(rgb(border_input))
700                                            .overflow_hidden()
701                                            .cursor_crosshair()
702                                            // Background is the hue at full saturation (HSV: S=100%, V=100%)
703                                            .bg(rgb(Hsv::new(hue, 100.0, 100.0).to_rgb().to_u32()))
704                                            .child(
705                                                // Canvas for gradients
706                                                canvas(
707                                                    move |bounds, _window, _cx| {
708                                                        canvas_origin_for_paint.set(bounds.origin);
709                                                        bounds
710                                                    },
711                                                    move |bounds, _prepaint_result, window, _cx| {
712                                                        // Paint white gradient (left to right): 90 degrees
713                                                        let white_start = linear_color_stop(white(), 0.0);
714                                                        let white_end = linear_color_stop(transparent_white(), 1.0);
715                                                        let white_gradient = linear_gradient(90.0, white_start, white_end);
716                                                        window.paint_quad(fill(bounds, white_gradient));
717
718                                                        // Paint black gradient (top to bottom): 180 degrees
719                                                        let black_start = linear_color_stop(transparent_black(), 0.0);
720                                                        let black_end = linear_color_stop(black(), 1.0);
721                                                        let black_gradient = linear_gradient(180.0, black_start, black_end);
722                                                        window.paint_quad(fill(bounds, black_gradient));
723                                                    },
724                                                )
725                                                .size_full()
726                                                .absolute()
727                                            )
728                                            // Crosshair indicator
729                                            .child({
730                                                // Position based on current S/V (HSV model)
731                                                let s = current_hsv.s / 100.0;
732                                                let v = current_hsv.v / 100.0;
733                                                let x = s * canvas_width;
734                                                let y = (1.0 - v) * canvas_height;
735
736                                                div()
737                                                    .absolute()
738                                                    .left(px(x - 6.0))
739                                                    .top(px(y - 6.0))
740                                                    .w(px(12.))
741                                                    .h(px(12.))
742                                                    .rounded_full()
743                                                    .border_2()
744                                                    .border_color(rgb(theme.bg_white))
745                                                    .shadow_sm()
746                                            })
747                                            .on_mouse_down(MouseButton::Left, {
748                                                let canvas_origin = canvas_origin_for_drag.clone();
749                                                cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
750                                                    let x: f32 = event.position.x.into();
751                                                    let y: f32 = event.position.y.into();
752                                                    this.handle_sl_at_position(x, y, canvas_origin.get(), canvas_width, canvas_height, cx);
753                                                })
754                                            })
755                                            .on_drag(
756                                                SlDrag {
757                                                    canvas_origin: canvas_origin_for_drag.clone(),
758                                                    canvas_width,
759                                                    canvas_height,
760                                                },
761                                                |_drag, _offset, _window, cx| cx.new(|_| EmptyDragView),
762                                            )
763                                            .on_drag_move(cx.listener(move |this, event: &DragMoveEvent<SlDrag>, _window, cx| {
764                                                let x: f32 = event.event.position.x.into();
765                                                let y: f32 = event.event.position.y.into();
766                                                let drag = event.drag(cx);
767                                                this.handle_sl_at_position(x, y, drag.canvas_origin.get(), drag.canvas_width, drag.canvas_height, cx);
768                                            }))
769                                    )
770                                    // Hue slider
771                                    .child(
772                                        div()
773                                            .flex()
774                                            .flex_row()
775                                            .items_center()
776                                            .gap_2()
777                                            .child(
778                                                div()
779                                                    .w(px(16.))
780                                                    .text_xs()
781                                                    .text_color(rgb(text_color))
782                                                    .child("H")
783                                            )
784                                            .child(
785                                                div()
786                                                    .id("hue_slider")
787                                                    .relative()
788                                                    .flex_1()
789                                                    .h(px(20.))
790                                                    .rounded_sm()
791                                                    .border_1()
792                                                    .border_color(rgb(border_input))
793                                                    .overflow_hidden()
794                                                    .cursor_pointer()
795                                                    .child(
796                                                        canvas(
797                                                            move |bounds, _window, _cx| {
798                                                                hue_origin_for_paint.set(bounds.origin.x.into());
799                                                                hue_width_for_paint.set(bounds.size.width.into());
800                                                                bounds
801                                                            },
802                                                            move |bounds, _prepaint_result, window, _cx| {
803                                                                // Paint rainbow gradient using multiple quads (90 degrees = left to right)
804                                                                let width: f32 = bounds.size.width.into();
805                                                                let segment_count = 6;
806                                                                let segment_width = width / segment_count as f32;
807                                                                let hue_colors = [
808                                                                    0xFF0000u32, // Red
809                                                                    0xFFFF00,    // Yellow
810                                                                    0x00FF00,    // Green
811                                                                    0x00FFFF,    // Cyan
812                                                                    0x0000FF,    // Blue
813                                                                    0xFF00FF,    // Magenta
814                                                                    0xFF0000,    // Red (wrap)
815                                                                ];
816
817                                                                for i in 0..segment_count {
818                                                                    let start_x = bounds.origin.x + px(i as f32 * segment_width);
819                                                                    let segment_bounds = Bounds {
820                                                                        origin: point(start_x, bounds.origin.y),
821                                                                        size: size(px(segment_width + 1.0), bounds.size.height),
822                                                                    };
823                                                                    let start_color = rgb(hue_colors[i]);
824                                                                    let end_color = rgb(hue_colors[i + 1]);
825                                                                    let start_stop = linear_color_stop(start_color, 0.0);
826                                                                    let end_stop = linear_color_stop(end_color, 1.0);
827                                                                    let gradient = linear_gradient(90.0, start_stop, end_stop);
828                                                                    window.paint_quad(fill(segment_bounds, gradient));
829                                                                }
830                                                            },
831                                                        )
832                                                        .size_full()
833                                                        .absolute()
834                                                    )
835                                                    // Handle - use measured width, accounting for handle width
836                                                    .child({
837                                                        let measured_width = hue_width.get();
838                                                        let handle_width = 4.0f32;
839                                                        // Handle moves within (0, measured_width - handle_width)
840                                                        // Use 359.0 as max to match the clamped hue range
841                                                        let handle_x = (current_hsv.h / 359.0).min(1.0) * (measured_width - handle_width);
842                                                        div()
843                                                            .absolute()
844                                                            .top_0()
845                                                            .bottom_0()
846                                                            .left(px(handle_x))
847                                                            .w(px(handle_width))
848                                                            .bg(rgb(theme.bg_white))
849                                                            .border_1()
850                                                            .border_color(rgb(theme.text_dark))
851                                                            .rounded_sm()
852                                                    })
853                                                    .on_mouse_down(MouseButton::Left, {
854                                                        let hue_origin = hue_origin_for_drag.clone();
855                                                        let hue_width = hue_width_for_drag.clone();
856                                                        cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
857                                                            let x: f32 = event.position.x.into();
858                                                            this.handle_hue_at_position(x, hue_origin.get(), hue_width.get(), cx);
859                                                        })
860                                                    })
861                                                    .on_drag(
862                                                        HueDrag {
863                                                            origin: hue_origin_for_drag.clone(),
864                                                            width: hue_width_for_drag.clone(),
865                                                        },
866                                                        |_drag, _offset, _window, cx| cx.new(|_| EmptyDragView),
867                                                    )
868                                                    .on_drag_move(cx.listener(move |this, event: &DragMoveEvent<HueDrag>, _window, cx| {
869                                                        let x: f32 = event.event.position.x.into();
870                                                        let drag = event.drag(cx);
871                                                        this.handle_hue_at_position(x, drag.origin.get(), drag.width.get(), cx);
872                                                    }))
873                                            )
874                                    )
875                                    // Alpha slider (if enabled)
876                                    .when(with_alpha, |parent| {
877                                        parent.child(
878                                            div()
879                                                .flex()
880                                                .flex_row()
881                                                .items_center()
882                                                .gap_2()
883                                                .child(
884                                                    div()
885                                                        .w(px(16.))
886                                                        .text_xs()
887                                                        .text_color(rgb(text_color))
888                                                        .child("A")
889                                                )
890                                                .child(
891                                                    div()
892                                                        .id("alpha_slider")
893                                                        .relative()
894                                                        .flex_1()
895                                                        .h(px(20.))
896                                                        .rounded_sm()
897                                                        .border_1()
898                                                        .border_color(rgb(border_input))
899                                                        .overflow_hidden()
900                                                        .cursor_pointer()
901                                                        // Checkerboard background for alpha visualization
902                                                        .child(Self::render_checkerboard())
903                                                        .child(
904                                                            canvas(
905                                                                move |bounds, _window, _cx| {
906                                                                    alpha_origin_for_paint.set(bounds.origin.x.into());
907                                                                    alpha_width_for_paint.set(bounds.size.width.into());
908                                                                    bounds
909                                                                },
910                                                                move |bounds, _prepaint_result, window, _cx| {
911                                                                    // Paint color gradient with transparency (90 degrees = left to right)
912                                                                    let color = rgb(current_rgb.to_u32());
913                                                                    let start_stop = linear_color_stop(transparent_white(), 0.0);
914                                                                    let end_stop = linear_color_stop(color, 1.0);
915                                                                    let gradient = linear_gradient(90.0, start_stop, end_stop);
916                                                                    window.paint_quad(fill(bounds, gradient));
917                                                                },
918                                                            )
919                                                            .size_full()
920                                                            .absolute()
921                                                        )
922                                                        // Handle - use measured width, accounting for handle width
923                                                        .child({
924                                                            let measured_width = alpha_width.get();
925                                                            let handle_width = 4.0f32;
926                                                            // Handle moves within (0, measured_width - handle_width)
927                                                            let handle_x = (current_alpha as f32 / 255.0) * (measured_width - handle_width);
928                                                            div()
929                                                                .absolute()
930                                                                .top_0()
931                                                                .bottom_0()
932                                                                .left(px(handle_x))
933                                                                .w(px(handle_width))
934                                                                .bg(rgb(theme.bg_white))
935                                                                .border_1()
936                                                                .border_color(rgb(theme.text_dark))
937                                                                .rounded_sm()
938                                                        })
939                                                        .on_mouse_down(MouseButton::Left, {
940                                                            let alpha_origin = alpha_origin_for_drag.clone();
941                                                            let alpha_width = alpha_width_for_drag.clone();
942                                                            cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
943                                                                let x: f32 = event.position.x.into();
944                                                                this.handle_alpha_at_position(x, alpha_origin.get(), alpha_width.get(), cx);
945                                                            })
946                                                        })
947                                                        .on_drag(
948                                                            AlphaDrag {
949                                                                origin: alpha_origin_for_drag.clone(),
950                                                                width: alpha_width_for_drag.clone(),
951                                                            },
952                                                            |_drag, _offset, _window, cx| cx.new(|_| EmptyDragView),
953                                                        )
954                                                        .on_drag_move(cx.listener(move |this, event: &DragMoveEvent<AlphaDrag>, _window, cx| {
955                                                            let x: f32 = event.event.position.x.into();
956                                                            let drag = event.drag(cx);
957                                                            this.handle_alpha_at_position(x, drag.origin.get(), drag.width.get(), cx);
958                                                        }))
959                                                )
960                                        )
961                                    })
962                                    // RGB sliders
963                                    .child(
964                                        Self::render_component_slider(
965                                            "R", current_rgb.r as f32, 255.0,
966                                            (0x000000, 0xFF0000),
967                                            |this, v, cx| this.update_from_rgb(v as u8, this.current_rgb.g, this.current_rgb.b, cx),
968                                            &theme, cx
969                                        )
970                                    )
971                                    .child(
972                                        Self::render_component_slider(
973                                            "G", current_rgb.g as f32, 255.0,
974                                            (0x000000, 0x00FF00),
975                                            |this, v, cx| this.update_from_rgb(this.current_rgb.r, v as u8, this.current_rgb.b, cx),
976                                            &theme, cx
977                                        )
978                                    )
979                                    .child(
980                                        Self::render_component_slider(
981                                            "B", current_rgb.b as f32, 255.0,
982                                            (0x000000, 0x0000FF),
983                                            |this, v, cx| this.update_from_rgb(this.current_rgb.r, this.current_rgb.g, v as u8, cx),
984                                            &theme, cx
985                                        )
986                                    )
987                                    // Old / New color comparison
988                                    .child(
989                                        div()
990                                            .flex()
991                                            .flex_row()
992                                            .items_center()
993                                            .gap_4()
994                                            .child(
995                                                div()
996                                                    .flex()
997                                                    .flex_col()
998                                                    .items_center()
999                                                    .gap_1()
1000                                                    .child(
1001                                                        div()
1002                                                            .text_xs()
1003                                                            .text_color(rgb(text_color))
1004                                                            .child("Old")
1005                                                    )
1006                                                    .child(
1007                                                        div()
1008                                                            .id("old_color_swatch")
1009                                                            .relative()
1010                                                            .w(px(60.))
1011                                                            .h(px(30.))
1012                                                            .border_1()
1013                                                            .border_color(rgb(border_input))
1014                                                            .rounded_md()
1015                                                            .overflow_hidden()
1016                                                            .cursor_pointer()
1017                                                            // Checkerboard for alpha
1018                                                            .when(with_alpha, |d| d.child(Self::render_checkerboard()))
1019                                                            .child(
1020                                                                div()
1021                                                                    .size_full()
1022                                                                    .absolute()
1023                                                                    .bg(original_color)
1024                                                            )
1025                                                            .on_click(cx.listener(|this, _event, _window, cx| {
1026                                                                this.set_value_internal(&this.original_value.clone(), cx);
1027                                                            }))
1028                                                    )
1029                                                    .child(
1030                                                        div()
1031                                                            .text_xs()
1032                                                            .text_color(rgb(text_color))
1033                                                            .child(original_hex)
1034                                                    )
1035                                            )
1036                                            .child(
1037                                                div()
1038                                                    .flex()
1039                                                    .flex_col()
1040                                                    .items_center()
1041                                                    .gap_1()
1042                                                    .child(
1043                                                        div()
1044                                                            .text_xs()
1045                                                            .text_color(rgb(text_color))
1046                                                            .child("New")
1047                                                    )
1048                                                    .child(
1049                                                        div()
1050                                                            .relative()
1051                                                            .w(px(60.))
1052                                                            .h(px(30.))
1053                                                            .border_1()
1054                                                            .border_color(rgb(border_input))
1055                                                            .rounded_md()
1056                                                            .overflow_hidden()
1057                                                            // Checkerboard for alpha
1058                                                            .when(with_alpha, |d| d.child(Self::render_checkerboard()))
1059                                                            .child(
1060                                                                div()
1061                                                                    .size_full()
1062                                                                    .absolute()
1063                                                                    .bg(new_color)
1064                                                            )
1065                                                    )
1066                                                    .child(
1067                                                        div()
1068                                                            .text_xs()
1069                                                            .text_color(rgb(text_color))
1070                                                            .child(new_hex)
1071                                                    )
1072                                            )
1073                                    )
1074                                    // Cancel / Apply buttons
1075                                    .child(
1076                                        div()
1077                                            .flex()
1078                                            .flex_row()
1079                                            .justify_end()
1080                                            .gap_2()
1081                                            .mt_2()
1082                                            .child(
1083                                                secondary_button("picker_cancel", "Cancel", cx)
1084                                                    .on_click(cx.listener(|this, _event, _window, cx| {
1085                                                        this.cancel_picker(cx);
1086                                                    }))
1087                                            )
1088                                            .child(
1089                                                primary_button("picker_apply", "Apply", true, cx)
1090                                                    .on_click(cx.listener(|this, _event, _window, cx| {
1091                                                        this.apply_picker(cx);
1092                                                    }))
1093                                            )
1094                                    )
1095                                    // Apply on click outside
1096                                    .on_mouse_down_out(cx.listener(|this, _event, _window, cx| {
1097                                        this.apply_picker(cx);
1098                                    }))
1099                            )
1100                    )
1101                )
1102            })
1103    }
1104}
1105
1106impl ColorSwatch {
1107    /// Render a checkerboard pattern canvas for alpha transparency visualization
1108    ///
1109    /// Creates an 8x8 pixel alternating light/dark grid that shows through
1110    /// transparent colors, helping users visualize alpha values.
1111    fn render_checkerboard() -> impl IntoElement {
1112        canvas(
1113            |bounds, _window, _cx| bounds,
1114            |bounds, _prepaint_result, window, _cx| {
1115                let cell_size = 8.0f32;
1116                let light = rgb(0xFFFFFF);
1117                let dark = rgb(0xCCCCCC);
1118                let width: f32 = bounds.size.width.into();
1119                let height: f32 = bounds.size.height.into();
1120                // Use saturating conversion to prevent overflow on extremely large bounds
1121                let cols = ((width / cell_size).ceil() as i32).max(0);
1122                let rows = ((height / cell_size).ceil() as i32).max(0);
1123
1124                for row in 0..rows {
1125                    for col in 0..cols {
1126                        let is_light = (row + col) % 2 == 0;
1127                        let color = if is_light { light } else { dark };
1128                        let x = bounds.origin.x + px(col as f32 * cell_size);
1129                        let y = bounds.origin.y + px(row as f32 * cell_size);
1130                        let cell_w = (cell_size).min(width - col as f32 * cell_size);
1131                        let cell_h = (cell_size).min(height - row as f32 * cell_size);
1132                        let cell_bounds = Bounds {
1133                            origin: point(x, y),
1134                            size: size(px(cell_w), px(cell_h)),
1135                        };
1136                        window.paint_quad(fill(cell_bounds, color));
1137                    }
1138                }
1139            },
1140        )
1141        .size_full()
1142        .absolute()
1143    }
1144
1145    /// Render a component slider for RGB values
1146    ///
1147    /// Creates a horizontal gradient slider with a draggable handle.
1148    /// Used for R, G, and B channel adjustment in the color picker.
1149    fn render_component_slider(
1150        label: &str,
1151        value: f32,
1152        max: f32,
1153        gradient_colors: (u32, u32),
1154        update_fn: fn(&mut ColorSwatch, f32, &mut Context<Self>),
1155        theme: &Theme,
1156        cx: &mut Context<Self>,
1157    ) -> impl IntoElement {
1158        let handle_content_width = 4.0f32;
1159        let handle_visual_width = 6.0f32; // 4px content + 2px border
1160        let value_display = value.round() as i32;
1161        let text_color = theme.text_primary;
1162        let border_input = theme.border_input;
1163        let handle_bg = theme.bg_white;
1164        let handle_border = theme.text_dark;
1165        let (start_color, end_color) = gradient_colors;
1166
1167        // Use Rc<Cell<f32>> for slider dimensions (like H slider)
1168        let slider_origin = Rc::new(Cell::new(0.0f32));
1169        let slider_width = Rc::new(Cell::new(200.0f32)); // Initial estimate
1170        let slider_origin_for_paint = slider_origin.clone();
1171        let slider_width_for_paint = slider_width.clone();
1172        let slider_origin_for_drag = slider_origin.clone();
1173        let slider_width_for_drag = slider_width.clone();
1174        let slider_width_for_handle = slider_width.clone();
1175        let slider_width_for_mouse = slider_width.clone();
1176
1177        div()
1178            .flex()
1179            .flex_row()
1180            .items_center()
1181            .gap_2()
1182            .child(
1183                div()
1184                    .w(px(16.))
1185                    .text_xs()
1186                    .text_color(rgb(text_color))
1187                    .child(label.to_string())
1188            )
1189            .child(
1190                div()
1191                    .id(SharedString::from(format!("comp_slider_{}", label)))
1192                    .relative()
1193                    .flex_1()
1194                    .h(px(20.))
1195                    .rounded_sm()
1196                    .border_1()
1197                    .border_color(rgb(border_input))
1198                    .overflow_hidden()
1199                    .cursor_pointer()
1200                    .child(
1201                        canvas(
1202                            move |bounds, _window, _cx| {
1203                                slider_origin_for_paint.set(bounds.origin.x.into());
1204                                slider_width_for_paint.set(bounds.size.width.into());
1205                                bounds
1206                            },
1207                            move |bounds, _prepaint_result, window, _cx| {
1208                                // 90 degrees = left to right
1209                                let start_stop = linear_color_stop(rgb(start_color), 0.0);
1210                                let end_stop = linear_color_stop(rgb(end_color), 1.0);
1211                                let gradient = linear_gradient(90.0, start_stop, end_stop);
1212                                window.paint_quad(fill(bounds, gradient));
1213                            },
1214                        )
1215                        .size_full()
1216                        .absolute()
1217                    )
1218                    // Handle - use measured width, accounting for handle width
1219                    .child({
1220                        let measured_width = slider_width_for_handle.get();
1221                        let handle_width = 4.0f32;
1222                        // Handle moves within (0, measured_width - handle_width)
1223                        let handle_x = (value / max) * (measured_width - handle_width);
1224                        div()
1225                            .absolute()
1226                            .top_0()
1227                            .bottom_0()
1228                            .left(px(handle_x))
1229                            .w(px(handle_content_width))
1230                            .bg(rgb(handle_bg))
1231                            .border_1()
1232                            .border_color(rgb(handle_border))
1233                            .rounded_sm()
1234                    })
1235                    .on_mouse_down(MouseButton::Left, {
1236                        let comp_origin = slider_origin_for_drag.clone();
1237                        let comp_width = slider_width_for_mouse.clone();
1238                        cx.listener(move |this, event: &MouseDownEvent, _window, cx| {
1239                            let x: f32 = event.position.x.into();
1240                            let origin = comp_origin.get();
1241                            let slider_w = comp_width.get();
1242                            let usable_width = slider_w - handle_visual_width;
1243                            if usable_width <= 0.0 {
1244                                return;
1245                            }
1246                            let rel_x = (x - origin - handle_visual_width / 2.0).clamp(0.0, usable_width);
1247                            let new_value = (rel_x / usable_width) * max;
1248                            update_fn(this, new_value, cx);
1249                        })
1250                    })
1251                    .on_drag(
1252                        ComponentDrag {
1253                            origin: slider_origin_for_drag.clone(),
1254                            width: slider_width_for_drag.clone(),
1255                            handle_visual_width,
1256                            max_value: max,
1257                            update_fn,
1258                        },
1259                        |_drag, _offset, _window, cx| cx.new(|_| EmptyDragView),
1260                    )
1261                    .on_drag_move(cx.listener(move |this, event: &DragMoveEvent<ComponentDrag>, _window, cx| {
1262                        let x: f32 = event.event.position.x.into();
1263                        let drag = event.drag(cx);
1264                        let origin = drag.origin.get();
1265                        let slider_w = drag.width.get();
1266                        let usable_width = slider_w - drag.handle_visual_width;
1267                        // Guard against division by zero
1268                        if usable_width <= 0.0 {
1269                            return;
1270                        }
1271                        // Map click position to handle left edge position, then to value
1272                        let rel_x = (x - origin - drag.handle_visual_width / 2.0).clamp(0.0, usable_width);
1273                        let new_value = (rel_x / usable_width) * drag.max_value;
1274                        (drag.update_fn)(this, new_value, cx);
1275                    }))
1276            )
1277            .child(
1278                div()
1279                    .w(px(28.))
1280                    .flex_shrink_0()
1281                    .text_xs()
1282                    .text_color(rgb(text_color))
1283                    .text_right()
1284                    .child(format!("{}", value_display))
1285            )
1286    }
1287}