git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/input/color_selector.rs (43.1K)

   1 use crate::colors;
   2 use crate::scene::layout::{Rect, Size};
   3 use crate::scene::paint::PaintCtx;
   4 use crate::widget::model::{Adapted, EventCtx, Input, Layout, Paint};
   5 use crate::widget::*;
   6 
   7 #[derive(Debug)]
   8 pub struct ColorSelector {
   9     pub color: [u8; 3],
  10     pub alpha: u8,
  11     just_clicked: bool,
  12     pub editing: bool,
  13     pub(crate) edit_buffer: String,
  14     pub cursor_idx: usize,
  15     pub font_family: String,
  16     pub command: String,
  17     hovered: bool,
  18     child: std::sync::Arc<std::sync::Mutex<Option<std::process::Child>>>,
  19     pub editor_state: TextEditorState,
  20     pub just_changed: bool,
  21     pub with_alpha: bool,
  22     /// Live color lines from the running picker (`cce-color-editor --stream` prints
  23     /// every change), forwarded by a reader thread — the value applies while
  24     /// the editor stays open instead of on exit.
  25     live_rx: Option<std::sync::mpsc::Receiver<String>>,
  26     /// The value at picker launch, restored when the stream reports `cancel`.
  27     revert_hex: Option<String>,
  28     /// Char-index → x offsets of the drawn hex text, recorded by
  29     /// [`Paint::prepare_text`] from the same shaped buffer `ctx.text` draws
  30     /// (size 12, default family). The caret reads these; the SVG-rasterized
  31     /// `measure_text` prefix it used before reports inked extent, which drifts
  32     /// off the glyph advances. Empty until the first shape.
  33     glyph_offsets: Vec<f32>,
  34     /// Recessed style: the hex field is a well carved into the plate below
  35     /// (the TextBox's, rim lit while editing) and the swatch a raised bevel
  36     /// plate of its colour, instead of the hairline frame and the flat swatch
  37     /// with its glow. Defaults to `control_relief()`.
  38     recessed: Option<bool>,
  39 }
  40 
  41 impl Clone for ColorSelector {
  42     fn clone(&self) -> Self {
  43         Self {
  44             color: self.color,
  45             alpha: self.alpha,
  46             just_clicked: self.just_clicked,
  47             editing: self.editing,
  48             edit_buffer: self.edit_buffer.clone(),
  49             cursor_idx: self.cursor_idx,
  50             font_family: self.font_family.clone(),
  51             command: self.command.clone(),
  52             hovered: self.hovered,
  53             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
  54             editor_state: self.editor_state.clone(),
  55             just_changed: self.just_changed,
  56             with_alpha: self.with_alpha,
  57             live_rx: None,
  58             revert_hex: None,
  59             glyph_offsets: self.glyph_offsets.clone(),
  60             recessed: self.recessed,
  61         }
  62     }
  63 }
  64 
  65 impl ColorSelector {
  66     /// The style in force: the per-widget override (`with_recessed`) when set, else
  67     /// the DE's `control_relief`, read live so a runtime switch
  68     /// (`layout::set_control_relief`) restyles every control at once.
  69     fn recessed(&self) -> bool {
  70         self.recessed.unwrap_or_else(crate::layout::control_relief)
  71     }
  72 
  73     pub fn new(color: [u8; 3]) -> Adapted<ColorSelector> {
  74         Adapted::new(ColorSelector {
  75             color,
  76             alpha: 255,
  77             just_clicked: false,
  78             editing: false,
  79             edit_buffer: String::new(),
  80             cursor_idx: 0,
  81             font_family: crate::layout::color_selector_font(),
  82             command: "cce-color-editor".to_string(),
  83             hovered: false,
  84             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
  85             editor_state: TextEditorState::new(String::new()),
  86             just_changed: false,
  87             with_alpha: false,
  88             live_rx: None,
  89             revert_hex: None,
  90             glyph_offsets: Vec::new(),
  91             recessed: None,
  92         })
  93     }
  94 
  95     pub fn new_rgba(color: [u8; 4]) -> Adapted<ColorSelector> {
  96         Adapted::new(ColorSelector {
  97             color: [color[0], color[1], color[2]],
  98             alpha: color[3],
  99             just_clicked: false,
 100             editing: false,
 101             edit_buffer: String::new(),
 102             cursor_idx: 0,
 103             font_family: crate::layout::color_selector_font(),
 104             command: "cce-color-editor".to_string(),
 105             hovered: false,
 106             child: std::sync::Arc::new(std::sync::Mutex::new(None)),
 107             editor_state: TextEditorState::new(String::new()),
 108             just_changed: false,
 109             with_alpha: true,
 110             live_rx: None,
 111             revert_hex: None,
 112             glyph_offsets: Vec::new(),
 113             recessed: None,
 114         })
 115     }
 116 
 117     /// The hex field's well for hosts that draw this control through the legacy
 118     /// flat views (see `ParametersBg::reliefs`): (x, y, w, h, radius, depth) over the
 119     /// widget's assigned content `rect`, or None when the style is off. The same
 120     /// geometry `paint` carves (untinted).
 121     pub fn field_relief(&self, rect: Rect) -> Option<(f32, f32, f32, f32, f32, f32)> {
 122         if !self.recessed() {
 123             return None;
 124         }
 125         let well_h = crate::layout::color_selector_height().min(rect.height);
 126         let radius = crate::layout::textbox_corner_radius();
 127         let depth = crate::layout::bevel_width().min(well_h * 0.2);
 128         // One well across the whole control: the hex text and the swatch are
 129         // segments of its floor (the Breadcrumb composition), not two parts.
 130         let (well, radii) = crate::layout::carve_inside(
 131             Rect { x: rect.x, y: rect.y, width: rect.width, height: well_h },
 132             (radius, radius, radius, radius),
 133             depth,
 134         );
 135         Some((well.x, well.y, well.width, well.height, radii.0, depth))
 136     }
 137 
 138 }
 139 
 140 impl Adapted<ColorSelector> {
 141     /// Recessed style: see the `recessed` field.
 142     pub fn with_recessed(mut self, recessed: bool) -> Self {
 143         self.recessed = Some(recessed);
 144         self
 145     }
 146 
 147     pub fn with_alpha(mut self, with_alpha: bool) -> Self {
 148         self.with_alpha = with_alpha;
 149         self
 150     }
 151 
 152     pub fn with_font_family(mut self, font_family: &str) -> Self {
 153         self.font_family = font_family.to_string();
 154         self
 155     }
 156 
 157     pub fn with_command(mut self, command: &str) -> Self {
 158         self.command = command.to_string();
 159         self
 160     }
 161 }
 162 
 163 impl ColorSelector {
 164     fn value_hex(&self) -> String {
 165         if self.with_alpha {
 166             Some(format!("#{:02x}{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2], self.alpha))
 167         } else {
 168             Some(format!("#{:02x}{:02x}{:02x}", self.color[0], self.color[1], self.color[2]))
 169         }
 170     
 171         .unwrap()
 172     }
 173 
 174     fn begin_edit(&mut self) {
 175         self.editing = true;
 176         self.edit_buffer = self.value_hex();
 177         self.cursor_idx = self.edit_buffer.chars().count();
 178     }
 179 }
 180 
 181 impl Layout for ColorSelector {
 182     fn intrinsic_size(&self) -> Option<Size> {
 183         Some(Size::new(0.0, crate::layout::color_selector_height()))
 184     }
 185 }
 186 
 187 impl Paint for ColorSelector {
 188     fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem, _rect: Rect) {
 189         // Shape the drawn hex string exactly as `ctx.text` draws it (size 12,
 190         // default family) and record char-index → x for the caret.
 191         let text = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
 192         let clusters =
 193             crate::backend::text::shaped_cluster_offsets(fs, &text, 12.0, None);
 194         let mut offsets = vec![0.0f32; text.chars().count() + 1];
 195         for (byte, x) in clusters {
 196             let ci = text[..byte.min(text.len())].chars().count();
 197             if ci < offsets.len() {
 198                 offsets[ci] = x;
 199             }
 200         }
 201         let mut current = 0.0;
 202         for off in offsets.iter_mut() {
 203             if *off == 0.0 {
 204                 *off = current;
 205             } else {
 206                 current = *off;
 207             }
 208         }
 209         self.glyph_offsets = offsets;
 210     }
 211 
 212     fn color(&self) -> [f32; 4] {
 213         colors::to_linear([
 214             self.color[0] as f32 / 255.0,
 215             self.color[1] as f32 / 255.0,
 216             self.color[2] as f32 / 255.0,
 217             self.alpha as f32 / 255.0,
 218         ])
 219     }
 220 
 221     fn widget_font(&self) -> Option<String> {
 222         Some(self.font_family.clone())
 223     }
 224 
 225     fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
 226         let r = crate::layout::color_selector_corner_radius();
 227         if r > 0.0 {
 228             Some((r, (true, true, true, true)))
 229         } else {
 230             None
 231         }
 232     }
 233 
 234     /// The legacy `extra_quads` body against the laid-out rect (field, caret while
 235     /// editing, and the soft-glow rounded color preview), plus the hex readout label.
 236     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 237         // The well is color_selector_height tall, seated at the content rect's TOP —
 238         // the label rides in the strip above it and the row's bottom band belongs
 239         // to the NEXT row's label. Hosts that hand
 240         // over a whole param row (ParametersBg's 40px color rows) get a
 241         // standard control-height well instead of a row-tall one.
 242         let well_h = crate::layout::color_selector_height().min(rect.height);
 243         let rect = Rect { x: rect.x, y: rect.y, width: rect.width, height: well_h };
 244         let mut quads: Vec<(f32, f32, f32, f32, [f32; 4])> = Vec::new();
 245         let visual_h = rect.height;
 246         let pick_x = rect.x + rect.width * 0.65;
 247         let pick_w = rect.width * 0.35;
 248         // Typing a hex value: the on-screen keyboard follows
 249         // (`crate::text_input`).
 250         if self.editing {
 251             let (ox, oy) = ctx.offset();
 252             crate::text_input::claim(rect.x + ox, rect.y + oy, pick_x - rect.x, visual_h);
 253         }
 254 
 255         // The text field has NO face of its own — a frame over the host plate,
 256         // like a relief TextBox well (transparent fill, the outline defines
 257         // it) and the closed-dropdown convention. The first colorless pass
 258         // used the textbox background here, but under the DE's relief themes
 259         // real text wells draw no fill, so even a neutral one read as "the
 260         // color selector has a background". Neutral greys for the frame; the
 261         // caret is the editing affordance.
 262         let border_color = crate::colors::well_frame_color(self.hovered, self.editing);
 263 
 264         // A real frame, not a border-quad-under-fill-quad: with no fill, the
 265         // old full-rect border quad would read as a solid slab. Rounded at the
 266         // selector's radius like the well it stands in for.
 267         if !self.recessed() {
 268             let fr = crate::layout::color_selector_corner_radius();
 269             ctx.border(rect, (fr, fr, fr, fr), [0.0; 4], border_color, 1.0);
 270         }
 271 
 272         if self.editing {
 273             let font_size = 12.0;
 274             let text_w = self.glyph_offsets.get(self.cursor_idx).copied().unwrap_or_else(|| {
 275                 let cursor_text: String = self.edit_buffer.chars().take(self.cursor_idx).collect();
 276                 crate::widget::display::measure_text(&cursor_text, font_size)
 277             });
 278             let caret_x = rect.x + crate::layout::CONTROL_TEXT_INSET + text_w;
 279             let caret_h = font_size * 1.15;
 280             let caret_y = rect.y + (visual_h - caret_h) / 2.0;
 281             quads.push((caret_x, caret_y, 1.5, caret_h, [0.80, 0.80, 0.85, 1.0]));
 282         }
 283 
 284         let linear_c = colors::to_linear([
 285             self.color[0] as f32 / 255.0,
 286             self.color[1] as f32 / 255.0,
 287             self.color[2] as f32 / 255.0,
 288             self.alpha as f32 / 255.0,
 289         ]);
 290         let border_w = 1.0;
 291         let border_c = colors::color_borders_color();
 292         
 293         let r = border_c[0];
 294         let g = border_c[1];
 295         let b = border_c[2];
 296 
 297         let preview_radius = crate::layout::color_selector_preview_corner_radius();
 298         let preview_margin = crate::layout::color_selector_preview_margin();
 299 
 300         let px = pick_x + preview_margin;
 301         let py = rect.y + preview_margin;
 302         let pw = (pick_w - 2.0 * preview_margin).max(0.0);
 303         let ph = (visual_h - 2.0 * preview_margin).max(0.0);
 304 
 305         let add_rounded_rect = |quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32| {
 306             let radius = radius.min(w * 0.5).min(h * 0.5);
 307             if radius <= 0.5 {
 308                 quads.push((x, y, w, h, color));
 309                 return;
 310             }
 311             
 312             quads.push((x + radius, y, w - 2.0 * radius, h, color));
 313             quads.push((x, y + radius, radius, h - 2.0 * radius, color));
 314             quads.push((x + w - radius, y + radius, radius, h - 2.0 * radius, color));
 315             
 316             let steps = radius.round() as i32;
 317             for i in 0..steps {
 318                 let dy = i as f32;
 319                 let next_dy = (i + 1) as f32;
 320                 
 321                 let cx = (radius * radius - (radius - dy) * (radius - dy)).sqrt();
 322                 let next_cx = (radius * radius - (radius - next_dy) * (radius - next_dy)).sqrt();
 323                 let avg_cx = (cx + next_cx) * 0.5;
 324                 
 325                 let strip_w = avg_cx;
 326                 let strip_h = 1.0f32;
 327                 
 328                 if strip_w > 0.0 {
 329                     quads.push((x + radius - strip_w, y + dy, strip_w, strip_h, color));
 330                     quads.push((x + w - radius, y + dy, strip_w, strip_h, color));
 331                     quads.push((x + radius - strip_w, y + h - dy - strip_h, strip_w, strip_h, color));
 332                     quads.push((x + w - radius, y + h - dy - strip_h, strip_w, strip_h, color));
 333                 }
 334             }
 335         };
 336 
 337         if self.recessed() {
 338             // The Breadcrumb composition: ONE well across the whole control,
 339             // the hex text on its floor at the left and the swatch as the
 340             // colour laid flush on the floor's right segment (the well's
 341             // rounded end is its own), the two parted by a seam groove. The
 342             // caret quad first (it rides on the floor), the carve after the
 343             // fills so the walls' shading falls over both, the seam last so it
 344             // dies into the well's rolled edge. Over a checker where the
 345             // colour carries alpha, so the transparency reads through.
 346             for (qx, qy, qw, qh, qc) in quads.drain(..) {
 347                 ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
 348             }
 349             let Some((wx, wy, ww, wh, radius, depth)) = self.field_relief(rect) else {
 350                 return;
 351             };
 352             let well = Rect { x: wx, y: wy, width: ww, height: wh };
 353             // The floor: past the wall's inner half-span (the ProgressBar's inset).
 354             let floor = Rect {
 355                 x: rect.x + depth,
 356                 y: rect.y + depth,
 357                 width: (rect.width - 2.0 * depth).max(0.0),
 358                 height: (visual_h - 2.0 * depth).max(0.0),
 359             };
 360             let seam_x = pick_x;
 361             let swatch = Rect { x: seam_x, y: floor.y, width: (floor.x + floor.width - seam_x).max(0.0), height: floor.height };
 362             let sr = crate::layout::textbox_corner_radius().min(swatch.height * 0.5);
 363             let right_end = (false, true, true, false);
 364             if self.with_alpha {
 365                 ctx.rounded_rect(swatch, sr, right_end, [0.8, 0.8, 0.8, 1.0]);
 366                 // The white cells, those at the right edge trimmed to the end's
 367                 // arc at their own row (the Breadcrumb's banded-wash sampling).
 368                 //
 369                 // Zero-radius rounded rects, NOT plain quads. The adapter's
 370                 // legacy plain-quad view (`own_plain_quads`) is this paint
 371                 // filtered to `Prim::Quad`, and a host that reads that view
 372                 // beside the paint — the designer, through ParametersBg's
 373                 // `plain_quads` — draws it AFTER the prims. As quads the
 374                 // cells came back a second time OVER the colour fill below,
 375                 // so every colour but white showed a checker as if it were
 376                 // transparent (invisible with the default white, 2026-09-21).
 377                 let grid = 6.0;
 378                 let cols = (swatch.width / grid).ceil() as i32;
 379                 let rows = (swatch.height / grid).ceil() as i32;
 380                 let arc = |yc: f32| -> f32 {
 381                     let dy = if yc < swatch.y + sr {
 382                         sr - (yc - swatch.y)
 383                     } else if yc > swatch.y + swatch.height - sr {
 384                         yc - (swatch.y + swatch.height - sr)
 385                     } else {
 386                         return 0.0;
 387                     };
 388                     sr - (sr * sr - dy * dy).max(0.0).sqrt()
 389                 };
 390                 for r in 0..rows {
 391                     for c in 0..cols {
 392                         if (r + c) % 2 == 1 {
 393                             let qx = swatch.x + c as f32 * grid;
 394                             let qy = swatch.y + r as f32 * grid;
 395                             let qh = grid.min(swatch.y + swatch.height - qy);
 396                             let right_edge = swatch.x + swatch.width - arc(qy + qh * 0.5);
 397                             let qw = grid.min(right_edge - qx);
 398                             if qw > 0.0 && qh > 0.0 {
 399                                 ctx.rounded_rect(
 400                                     Rect { x: qx, y: qy, width: qw, height: qh },
 401                                     0.0,
 402                                     (false, false, false, false),
 403                                     [1.0, 1.0, 1.0, 1.0],
 404                                 );
 405                             }
 406                         }
 407                     }
 408                 }
 409             }
 410             ctx.rounded_rect(swatch, sr, right_end, linear_c);
 411             let radii = (radius, radius, radius, radius);
 412             if self.editing {
 413                 let hc = crate::color::highlight_primary_color();
 414                 ctx.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
 415             } else {
 416                 ctx.recess(well, radii, depth);
 417             }
 418             ctx.groove(
 419                 (seam_x, well.y),
 420                 (seam_x, well.y + well.height),
 421                 crate::widget::Breadcrumb::SEAM_WIDTH,
 422                 depth,
 423                 well,
 424             );
 425             let hex = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
 426             // Bounded by the seam: the field is the well left of it, and while
 427             // `editing` this holds whatever has been typed, not a 7-character
 428             // hex code.
 429             ctx.text_with(
 430                 hex,
 431                 rect.x + crate::layout::CONTROL_TEXT_INSET,
 432                 crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0),
 433                 12.0,
 434                 [0xcc, 0xcc, 0xd4],
 435                 None,
 436                 Some([rect.x, rect.y, seam_x, rect.y + rect.height]),
 437             );
 438             return;
 439         }
 440 
 441         let steps = 6;
 442         for i in (1..=steps).rev() {
 443             let offset = i as f32 * 0.75;
 444             let rx = px - offset;
 445             let ry = py - offset;
 446             let rw = pw + 2.0 * offset;
 447             let rh = ph + 2.0 * offset;
 448             let alpha = 0.08 * (1.0 - (i as f32 / steps as f32).powf(1.5));
 449             if alpha > 0.001 {
 450                 add_rounded_rect(&mut quads, [r, g, b, alpha], rx, ry, rw, rh, preview_radius + offset);
 451             }
 452         }
 453 
 454         add_rounded_rect(&mut quads, border_c, px, py, pw, ph, preview_radius);
 455 
 456         if self.with_alpha {
 457             add_rounded_rect(
 458                 &mut quads,
 459                 [0.8, 0.8, 0.8, 1.0],
 460                 px + border_w,
 461                 py + border_w,
 462                 (pw - 2.0 * border_w).max(0.0),
 463                 (ph - 2.0 * border_w).max(0.0),
 464                 (preview_radius - border_w).max(0.0),
 465             );
 466             
 467             let grid_size = 6.0;
 468             let start_x = px + border_w;
 469             let start_y = py + border_w;
 470             let inner_w = (pw - 2.0 * border_w).max(0.0);
 471             let inner_h = (ph - 2.0 * border_w).max(0.0);
 472             
 473             let cols = (inner_w / grid_size).ceil() as i32;
 474             let rows = (inner_h / grid_size).ceil() as i32;
 475             for r in 0..rows {
 476                 for c in 0..cols {
 477                     if (r + c) % 2 == 1 {
 478                         let qx = start_x + c as f32 * grid_size;
 479                         let qy = start_y + r as f32 * grid_size;
 480                         let qw = grid_size.min(start_x + inner_w - qx);
 481                         let qh = grid_size.min(start_y + inner_h - qy);
 482                         if qw > 0.0 && qh > 0.0 {
 483                             quads.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
 484                         }
 485                     }
 486                 }
 487             }
 488         }
 489 
 490         add_rounded_rect(
 491             &mut quads,
 492             linear_c,
 493             px + border_w,
 494             py + border_w,
 495             (pw - 2.0 * border_w).max(0.0),
 496             (ph - 2.0 * border_w).max(0.0),
 497             (preview_radius - border_w).max(0.0),
 498         );
 499     
 500         for (qx, qy, qw, qh, qc) in quads {
 501             ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
 502         }
 503 
 504         let hex = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
 505         ctx.text_with(
 506             hex,
 507             rect.x + crate::layout::CONTROL_TEXT_INSET,
 508             crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0),
 509             12.0,
 510             [0xcc, 0xcc, 0xd4],
 511             None,
 512             Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]),
 513         );
 514     }
 515 }
 516 
 517 impl Input for ColorSelector {
 518     fn focus_role(&self) -> crate::widget::FocusRole {
 519         crate::widget::FocusRole::Well
 520     }
 521     fn opens_context_menu(&self) -> bool {
 522         true
 523     }
 524 
 525     fn take_click(&mut self) -> bool {
 526         if self.just_clicked { self.just_clicked = false; true } else { false }
 527     }
 528 
 529     fn take_change(&mut self) -> bool {
 530         let ret = self.just_changed;
 531         self.just_changed = false;
 532         ret
 533     }
 534 
 535     fn value_string(&self) -> Option<String> {
 536         Some(self.value_hex())
 537     }
 538 
 539     fn a11y_text(&self) -> Option<crate::a11y::A11yText> {
 540         // The hex field: what it shows, its caret while it is being typed into.
 541         let text = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
 542         let caret = self.cursor_idx.min(text.chars().count());
 543         Some(crate::a11y::A11yText {
 544             text,
 545             selection: self.editing.then_some((caret, caret)),
 546             editable: true,
 547             kind: Some(crate::l10n::tr("a11y-colour")),
 548             ..Default::default()
 549         })
 550     }
 551 
 552     fn a11y_set_text(&mut self, text: &str) -> bool {
 553         // A colour, as the field takes one when typed: a hex it parses, or nothing.
 554         let text = text.trim();
 555         if self.set_value_string(text) {
 556             return true;
 557         }
 558         // The colour it already holds, set while a half-typed hex is up: the field shows it.
 559         if self.editing && parse_hex(text).is_some() && self.edit_buffer != self.value_hex() {
 560             self.edit_buffer = self.value_hex();
 561             self.cursor_idx = self.edit_buffer.chars().count();
 562             return true;
 563         }
 564         false
 565     }
 566 
 567     fn set_value_string(&mut self, val: &str) -> bool {
 568         if let Some(c) = parse_hex(val) {
 569             let target_color = [c[0], c[1], c[2]];
 570             let target_alpha = if self.with_alpha { c[3] } else { 255 };
 571             if self.color != target_color || (self.with_alpha && self.alpha != target_alpha) {
 572                 self.color = target_color;
 573                 self.alpha = target_alpha;
 574                 self.just_changed = true;
 575                 if self.editing {
 576                     self.edit_buffer = self.value_hex();
 577                     self.cursor_idx = self.edit_buffer.chars().count();
 578                 }
 579                 return true;
 580             }
 581         }
 582         false
 583     
 584     }
 585 
 586     fn wants_tick(&self) -> bool {
 587         true
 588     }
 589 
 590     fn tick(&mut self, _dt: f32, _rect: Rect) -> bool {
 591         // Apply streamed picker lines as they arrive — the color changes live
 592         // while the editor stays open. The picker's Apply prints a final line
 593         // (already applied here); Cancel prints `cancel`, restoring the value
 594         // the picker launched with.
 595         let mut redraw = false;
 596         if let Some(rx) = &self.live_rx {
 597             // A live picker session is activity: the runner sleeps between
 598             // ticks when nothing is animating, and these lines come from a
 599             // reader thread it cannot see, so keep the frame cadence for as
 600             // long as the picker is open.
 601             redraw = true;
 602             let mut lines = Vec::new();
 603             let mut disconnected = false;
 604             loop {
 605                 match rx.try_recv() {
 606                     Ok(line) => lines.push(line),
 607                     Err(std::sync::mpsc::TryRecvError::Empty) => break,
 608                     Err(std::sync::mpsc::TryRecvError::Disconnected) => {
 609                         disconnected = true;
 610                         break;
 611                     }
 612                 }
 613             }
 614             if disconnected {
 615                 self.live_rx = None;
 616             }
 617             for line in lines {
 618                 let line = line.trim().to_string();
 619                 let target = if line == "cancel" {
 620                     self.revert_hex.clone().and_then(|h| parse_hex(&h))
 621                 } else {
 622                     parse_hex(&line)
 623                 };
 624                 if let Some(c) = target {
 625                     let color = [c[0], c[1], c[2]];
 626                     let alpha = if self.with_alpha { c[3] } else { 255 };
 627                     if self.color != color || self.alpha != alpha {
 628                         self.color = color;
 629                         self.alpha = alpha;
 630                         self.just_changed = true;
 631                         redraw = true;
 632                     }
 633                 }
 634             }
 635         }
 636         let mut child_opt = self.child.lock().unwrap();
 637         if let Some(ref mut child) = *child_opt {
 638             match child.try_wait() {
 639                 Ok(Some(_status)) => {
 640                     // The reader thread owns stdout and has already forwarded
 641                     // every line (including Apply's final one) — just reap.
 642                     *child_opt = None;
 643                 }
 644                 Ok(None) => {}
 645                 Err(e) => {
 646                     log::error!("Error checking color selector child process: {:?}", e);
 647                     *child_opt = None;
 648                 }
 649             }
 650         }
 651         redraw
 652     }
 653 
 654     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
 655         match event {
 656             Event::MouseButton { button, state, x, y, .. } => {
 657                 if *button != MouseButton::Left {
 658                     return false;
 659                 }
 660                 if *state != ElementState::Pressed {
 661                     return false;
 662                 }
 663                 let (px, py) = (*x, *y);
 664                 let _ = py;
 665                 let rect = ectx.rect;
 666                 if px >= rect.x + rect.width * 0.65 {
 667             let hex = self.value_hex();
 668             let mut child_guard = self.child.lock().unwrap();
 669             if let Some(mut old_child) = child_guard.take() {
 670                 let _ = old_child.kill();
 671             }
 672 
 673             let cmd_path = if let Ok(mut exe_path) = std::env::current_exe() {
 674                 exe_path.pop(); // remove executable name
 675                 let local_path = exe_path.join(&self.command);
 676                 if local_path.exists() {
 677                     local_path.to_string_lossy().into_owned()
 678                 } else {
 679                     let home = std::env::var("HOME").unwrap_or_default();
 680                     let local_bin = std::path::Path::new(&home).join(".local/bin").join(&self.command);
 681                     if local_bin.exists() {
 682                         local_bin.to_string_lossy().into_owned()
 683                     } else {
 684                         self.command.clone()
 685                     }
 686                 }
 687             } else {
 688                 self.command.clone()
 689             };
 690 
 691             place_picker_at_pointer(&self.command);
 692 
 693             let mut cmd = std::process::Command::new(&cmd_path);
 694             cmd.arg(&hex);
 695             if self.with_alpha {
 696                 cmd.arg("--alpha");
 697             }
 698             // Live picking: the picker streams every change on stdout; a
 699             // reader thread forwards lines so `tick` applies them while the
 700             // editor stays open. `cancel` restores the launch value.
 701             cmd.arg("--stream");
 702             if let Ok(mut child) = cmd.stdout(std::process::Stdio::piped()).spawn() {
 703                 if let Some(stdout) = child.stdout.take() {
 704                     let (tx, rx) = std::sync::mpsc::channel::<String>();
 705                     std::thread::spawn(move || {
 706                         use std::io::BufRead;
 707                         let reader = std::io::BufReader::new(stdout);
 708                         for line in reader.lines().map_while(Result::ok) {
 709                             if tx.send(line).is_err() {
 710                                 break;
 711                             }
 712                         }
 713                     });
 714                     self.live_rx = Some(rx);
 715                     self.revert_hex = Some(hex.clone());
 716                 }
 717                 *child_guard = Some(child);
 718             }
 719                     return true;
 720                 }
 721                 ectx.request_focus();
 722                 true
 723             }
 724             Event::KeyInput(event) => {
 725                 if event.state != ElementState::Pressed {
 726                     return false;
 727                 }
 728                 if !self.editing {
 729                     return false;
 730                 }
 731 
 732 
 733         
 734         let mut state = TextEditorState {
 735             buffer: self.edit_buffer.clone(),
 736             cursor_idx: self.cursor_idx,
 737             select_anchor: None,
 738             all_selected: false,
 739         };
 740         
 741         let mut handled = false;
 742         match &event.logical_key {
 743             Key::Named(NamedKey::Backspace) => {
 744                 state.delete_backwards();
 745                 handled = true;
 746             }
 747             Key::Named(NamedKey::Delete) => {
 748                 state.delete_forwards();
 749                 handled = true;
 750             }
 751             Key::Named(NamedKey::ArrowLeft) => {
 752                 state.move_cursor_left(false);
 753                 handled = true;
 754             }
 755             Key::Named(NamedKey::ArrowRight) => {
 756                 state.move_cursor_right(false);
 757                 handled = true;
 758             }
 759             Key::Named(NamedKey::Enter) => {
 760                 if let Some(c) = parse_hex(&state.buffer) {
 761                     self.color = [c[0], c[1], c[2]];
 762                     self.alpha = if self.with_alpha { c[3] } else { 255 };
 763                 }
 764                 self.editing = false;
 765                 handled = true;
 766             }
 767             Key::Named(NamedKey::Escape) => {
 768                 self.editing = false;
 769                 handled = true;
 770             }
 771             _ => {
 772                 if let Some(text) = &event.text {
 773                     for ch in text.chars() {
 774                         match ch {
 775                             '#' => {
 776                                 // A "#" only leads the value.
 777                                 if state.buffer.is_empty() || (state.cursor_idx == 0 && !state.buffer.starts_with('#')) {
 778                                     state.insert_text("#");
 779                                     handled = true;
 780                                 }
 781                             }
 782                             '0'..='9' | 'a'..='f' | 'A'..='F' => {
 783                                 let count = state.buffer.chars().count();
 784                                 let max_len = if state.buffer.starts_with('#') {
 785                                     if self.with_alpha { 9 } else { 7 }
 786                                 } else {
 787                                     if self.with_alpha { 8 } else { 6 }
 788                                 };
 789                                 if count < max_len {
 790                                     state.insert_text(&ch.to_ascii_lowercase().to_string());
 791                                     handled = true;
 792                                 }
 793                             }
 794                             _ => {}
 795                         }
 796                     }
 797                 }
 798             }
 799         }
 800         
 801         if self.editing {
 802             self.edit_buffer = state.buffer;
 803             self.cursor_idx = state.cursor_idx;
 804         }
 805         handled
 806     
 807             }
 808             Event::MouseEnter => {
 809                 self.hovered = true;
 810                 false
 811             }
 812             Event::MouseLeave => {
 813                 self.hovered = false;
 814                 false
 815             }
 816             Event::FocusIn => {
 817                 self.begin_edit();
 818                 false
 819             }
 820             Event::FocusOut => {
 821                 if self.editing {
 822                     self.editing = false;
 823                     if let Some(c) = parse_hex(&self.edit_buffer) {
 824                         self.color = [c[0], c[1], c[2]];
 825                         self.alpha = if self.with_alpha { c[3] } else { 255 };
 826                     }
 827                 }
 828                 false
 829             }
 830             _ => false,
 831         }
 832     }
 833 }
 834 
 835 fn parse_hex(s: &str) -> Option<[u8; 4]> {
 836     crate::color::parse_hex_bytes(s)
 837 }
 838 
 839 /// Ask the compositor to open the picker at this control instead of its
 840 /// remembered position. Native only: in a browser there is no compositor to ask.
 841 #[cfg(not(target_arch = "wasm32"))]
 842 fn place_picker_at_pointer(command: &str) {
 843     // The pointer is on the swatch right now, so its location IS the
 844     // control's location. One-shot, best-effort (`place-next` consumed at
 845     // the picker's map; ignored off-cce).
 846     if let Ok(reply) = crate::ipc::send_command("cce", "pointer-location") {
 847         let mut px = None;
 848         let mut py = None;
 849         for tok in reply.split_whitespace() {
 850             if let Some(v) = tok.strip_prefix("x=") {
 851                 px = v.parse::<f64>().ok();
 852             } else if let Some(v) = tok.strip_prefix("y=") {
 853                 py = v.parse::<f64>().ok();
 854             }
 855         }
 856         if let (Some(x), Some(y)) = (px, py) {
 857             let app_id = std::path::Path::new(&command)
 858                 .file_name()
 859                 .map(|n| n.to_string_lossy().into_owned())
 860                 .unwrap_or_else(|| command.to_string());
 861             let _ = crate::ipc::send_command(
 862                 "cce",
 863                 &format!("place-next {} {:.0} {:.0}", app_id, x, y),
 864             );
 865         }
 866     }
 867 }
 868 
 869 #[cfg(target_arch = "wasm32")]
 870 fn place_picker_at_pointer(_command: &str) {}
 871 
 872 #[cfg(test)]
 873 mod tests {
 874     use super::*;
 875 
 876     #[test]
 877     fn test_colorselector_keyboard_navigation() {
 878         let mut dummy = crate::context::UiContext::new();
 879         let mut cs = ColorSelector::new([255, 0, 0]);
 880         assert_eq!(cs.color, [255, 0, 0]);
 881         assert_eq!(cs.alpha, 255);
 882         assert!(!cs.editing);
 883 
 884         // 1. Focus color selector
 885         cs.focus();
 886         assert!(cs.editing);
 887         assert_eq!(cs.edit_buffer, "#ff0000");
 888         assert_eq!(cs.cursor_idx, 7);
 889 
 890         // 2. Backspace deletes character before cursor
 891         let backspace_ev = KeyEvent {
 892             state: ElementState::Pressed,
 893             logical_key: Key::Named(NamedKey::Backspace),
 894             text: None,
 895             repeat: false,
 896             ctrl: false,
 897             shift: false,
 898             alt: false,
 899         };
 900         let handled = cs.keyboard_input(&backspace_ev, &mut dummy);
 901         assert!(handled);
 902         assert_eq!(cs.edit_buffer, "#ff000");
 903         assert_eq!(cs.cursor_idx, 6);
 904 
 905         // 3. ArrowLeft moves cursor index
 906         let left_ev = KeyEvent {
 907             state: ElementState::Pressed,
 908             logical_key: Key::Named(NamedKey::ArrowLeft),
 909             text: None,
 910             repeat: false,
 911             ctrl: false,
 912             shift: false,
 913             alt: false,
 914         };
 915         let handled = cs.keyboard_input(&left_ev, &mut dummy);
 916         assert!(handled);
 917         assert_eq!(cs.cursor_idx, 5);
 918 
 919         // 4. Backspace at cursor index 5
 920         let handled = cs.keyboard_input(&backspace_ev, &mut dummy);
 921         assert!(handled);
 922         assert_eq!(cs.edit_buffer, "#ff00");
 923         assert_eq!(cs.cursor_idx, 4);
 924 
 925         // 5. ArrowRight moves cursor index
 926         let right_ev = KeyEvent {
 927             state: ElementState::Pressed,
 928             logical_key: Key::Named(NamedKey::ArrowRight),
 929             text: None,
 930             repeat: false,
 931             ctrl: false,
 932             shift: false,
 933             alt: false,
 934         };
 935         let handled = cs.keyboard_input(&right_ev, &mut dummy);
 936         assert!(handled);
 937         assert_eq!(cs.cursor_idx, 5);
 938 
 939         // 6. Delete deletes character at cursor
 940         let delete_ev = KeyEvent {
 941             state: ElementState::Pressed,
 942             logical_key: Key::Named(NamedKey::Delete),
 943             text: None,
 944             repeat: false,
 945             ctrl: false,
 946             shift: false,
 947             alt: false,
 948         };
 949         // Move cursor to index 3
 950         let handled = cs.keyboard_input(&left_ev, &mut dummy); // 4
 951         assert!(handled);
 952         let handled = cs.keyboard_input(&left_ev, &mut dummy); // 3
 953         assert!(handled);
 954         assert_eq!(cs.cursor_idx, 3);
 955         // buffer is "#ff0", index 3 points to the first '0'. Let's delete it.
 956         let handled = cs.keyboard_input(&delete_ev, &mut dummy);
 957         assert!(handled);
 958         assert_eq!(cs.edit_buffer, "#ff0");
 959         assert_eq!(cs.cursor_idx, 3);
 960 
 961         // 7. Typing hex character inserts at cursor index
 962         let type_b_ev = KeyEvent {
 963             state: ElementState::Pressed,
 964             logical_key: Key::Character("b".to_string()),
 965             text: Some("b".to_string()),
 966             repeat: false,
 967             ctrl: false,
 968             shift: false,
 969             alt: false,
 970         };
 971         let handled = cs.keyboard_input(&type_b_ev, &mut dummy);
 972         assert!(handled);
 973         assert_eq!(cs.edit_buffer, "#ffb0");
 974         assert_eq!(cs.cursor_idx, 4);
 975 
 976         // Move to index 1 and insert a 'c'
 977         for _ in 0..3 {
 978             let handled = cs.keyboard_input(&left_ev, &mut dummy);
 979             assert!(handled);
 980         }
 981         assert_eq!(cs.cursor_idx, 1);
 982         let type_c_ev = KeyEvent {
 983             state: ElementState::Pressed,
 984             logical_key: Key::Character("c".to_string()),
 985             text: Some("c".to_string()),
 986             repeat: false,
 987             ctrl: false,
 988             shift: false,
 989             alt: false,
 990         };
 991         let handled = cs.keyboard_input(&type_c_ev, &mut dummy);
 992         assert!(handled);
 993         assert_eq!(cs.edit_buffer, "#cffb0");
 994         assert_eq!(cs.cursor_idx, 2);
 995 
 996         // 8. Enter commits the color
 997         let type_5_ev = KeyEvent {
 998             state: ElementState::Pressed,
 999             logical_key: Key::Character("5".to_string()),
1000             text: Some("5".to_string()),
1001             repeat: false,
1002             ctrl: false,
1003             shift: false,
1004             alt: false,
1005         };
1006         // Move to index 5
1007         for _ in 0..4 {
1008             cs.keyboard_input(&right_ev, &mut dummy);
1009         }
1010         assert_eq!(cs.cursor_idx, 6);
1011         cs.keyboard_input(&type_5_ev, &mut dummy);
1012         assert_eq!(cs.edit_buffer, "#cffb05");
1013         assert_eq!(cs.cursor_idx, 7);
1014 
1015         let enter_ev = KeyEvent {
1016             state: ElementState::Pressed,
1017             logical_key: Key::Named(NamedKey::Enter),
1018             text: None,
1019             repeat: false,
1020             ctrl: false,
1021             shift: false,
1022             alt: false,
1023         };
1024         let handled = cs.keyboard_input(&enter_ev, &mut dummy);
1025         assert!(handled);
1026         assert!(!cs.editing);
1027         assert_eq!(cs.color, [0xcf, 0xfb, 0x05]);
1028         assert_eq!(cs.alpha, 255);
1029     }
1030 
1031     #[test]
1032     fn test_colorselector_alpha_support() {
1033         let mut dummy = crate::context::UiContext::new();
1034         let mut cs = ColorSelector::new_rgba([255, 0, 0, 128]);
1035         assert_eq!(cs.color, [255, 0, 0]);
1036         assert_eq!(cs.alpha, 128);
1037         assert!(cs.with_alpha);
1038 
1039         cs.focus();
1040         assert!(cs.editing);
1041         assert_eq!(cs.edit_buffer, "#ff000080");
1042         assert_eq!(cs.cursor_idx, 9);
1043 
1044         let type_a_ev = KeyEvent {
1045             state: ElementState::Pressed,
1046             logical_key: Key::Character("a".to_string()),
1047             text: Some("a".to_string()),
1048             repeat: false,
1049             ctrl: false,
1050             shift: false,
1051             alt: false,
1052         };
1053         let backspace_ev = KeyEvent {
1054             state: ElementState::Pressed,
1055             logical_key: Key::Named(NamedKey::Backspace),
1056             text: None,
1057             repeat: false,
1058             ctrl: false,
1059             shift: false,
1060             alt: false,
1061         };
1062         cs.keyboard_input(&backspace_ev, &mut dummy);
1063         cs.keyboard_input(&backspace_ev, &mut dummy);
1064         assert_eq!(cs.edit_buffer, "#ff0000");
1065 
1066         let type_b_ev = KeyEvent {
1067             state: ElementState::Pressed,
1068             logical_key: Key::Character("b".to_string()),
1069             text: Some("b".to_string()),
1070             repeat: false,
1071             ctrl: false,
1072             shift: false,
1073             alt: false,
1074         };
1075         cs.keyboard_input(&type_a_ev, &mut dummy);
1076         cs.keyboard_input(&type_b_ev, &mut dummy);
1077         assert_eq!(cs.edit_buffer, "#ff0000ab");
1078 
1079         let enter_ev = KeyEvent {
1080             state: ElementState::Pressed,
1081             logical_key: Key::Named(NamedKey::Enter),
1082             text: None,
1083             repeat: false,
1084             ctrl: false,
1085             shift: false,
1086             alt: false,
1087         };
1088         cs.keyboard_input(&enter_ev, &mut dummy);
1089         assert!(!cs.editing);
1090         assert_eq!(cs.color, [255, 0, 0]);
1091         assert_eq!(cs.alpha, 171);
1092     }
1093     /// The alpha preview's checker must not reach the legacy plain-quad
1094     /// view: a host that draws that view beside the paint draws it AFTER the
1095     /// prims, and the white cells then landed over the colour fill — a black
1096     /// swatch read as a black-and-white checker. The paint keeps its order
1097     /// (base, cells, colour on top) and the plain view carries no cells.
1098     #[test]
1099     fn alpha_checker_stays_out_of_the_plain_quad_view() {
1100         use crate::scene::paint::Prim;
1101         let mut cs = ColorSelector::new_rgba([0, 0, 0, 255]);
1102         cs.recessed = Some(true);
1103         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 40.0 };
1104         crate::widget::WidgetHost::set_rect(&mut cs, rect.x, rect.y, rect.width, rect.height);
1105         let mut pc = PaintCtx::new();
1106         let inner: &ColorSelector = &cs;
1107         crate::widget::Paint::paint(inner, rect, &mut pc);
1108         let items = pc.finish().items;
1109         let last_fill = items
1110             .iter()
1111             .rposition(|it| matches!(it.prim, Prim::RoundedRect { color, .. } if color == [0.0, 0.0, 0.0, 1.0]))
1112             .expect("the colour fill is painted");
1113         let white_cells: Vec<usize> = items
1114             .iter()
1115             .enumerate()
1116             .filter(|(_, it)| matches!(it.prim, Prim::RoundedRect { color, radius, .. } if color == [1.0; 4] && radius == 0.0))
1117             .map(|(i, _)| i)
1118             .collect();
1119         assert!(!white_cells.is_empty(), "the checker is painted");
1120         assert!(white_cells.iter().all(|&i| i < last_fill), "every cell is under the colour fill");
1121         assert!(
1122             !items.iter().any(|it| matches!(it.prim, Prim::Quad { color, .. } if color == [1.0; 4])),
1123             "no cell is a plain quad, so the legacy plain view cannot carry it"
1124         );
1125         let plain = crate::widget::shown_quads(&cs);
1126         assert!(plain.iter().all(|q| q.4 != [1.0, 1.0, 1.0, 1.0]), "the plain view has no white cells: {plain:?}");
1127     }
1128 
1129 }