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 (41.8K)

   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::window_runner::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 
 249         // The text field has NO face of its own — a frame over the host plate,
 250         // like a relief TextBox well (transparent fill, the outline defines
 251         // it) and the closed-dropdown convention. The first colorless pass
 252         // used the textbox background here, but under the DE's relief themes
 253         // real text wells draw no fill, so even a neutral one read as "the
 254         // color selector has a background". Neutral greys for the frame; the
 255         // caret is the editing affordance.
 256         let border_color = crate::colors::well_frame_color(self.hovered, self.editing);
 257 
 258         // A real frame, not a border-quad-under-fill-quad: with no fill, the
 259         // old full-rect border quad would read as a solid slab. Rounded at the
 260         // selector's radius like the well it stands in for.
 261         if !self.recessed() {
 262             let fr = crate::layout::color_selector_corner_radius();
 263             ctx.border(rect, (fr, fr, fr, fr), [0.0; 4], border_color, 1.0);
 264         }
 265 
 266         if self.editing {
 267             let font_size = 12.0;
 268             let text_w = self.glyph_offsets.get(self.cursor_idx).copied().unwrap_or_else(|| {
 269                 let cursor_text: String = self.edit_buffer.chars().take(self.cursor_idx).collect();
 270                 crate::widget::display::measure_text(&cursor_text, font_size)
 271             });
 272             let caret_x = rect.x + crate::layout::CONTROL_TEXT_INSET + text_w;
 273             let caret_h = font_size * 1.15;
 274             let caret_y = rect.y + (visual_h - caret_h) / 2.0;
 275             quads.push((caret_x, caret_y, 1.5, caret_h, [0.80, 0.80, 0.85, 1.0]));
 276         }
 277 
 278         let linear_c = colors::to_linear([
 279             self.color[0] as f32 / 255.0,
 280             self.color[1] as f32 / 255.0,
 281             self.color[2] as f32 / 255.0,
 282             self.alpha as f32 / 255.0,
 283         ]);
 284         let border_w = 1.0;
 285         let border_c = colors::color_borders_color();
 286         
 287         let r = border_c[0];
 288         let g = border_c[1];
 289         let b = border_c[2];
 290 
 291         let preview_radius = crate::layout::color_selector_preview_corner_radius();
 292         let preview_margin = crate::layout::color_selector_preview_margin();
 293 
 294         let px = pick_x + preview_margin;
 295         let py = rect.y + preview_margin;
 296         let pw = (pick_w - 2.0 * preview_margin).max(0.0);
 297         let ph = (visual_h - 2.0 * preview_margin).max(0.0);
 298 
 299         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| {
 300             let radius = radius.min(w * 0.5).min(h * 0.5);
 301             if radius <= 0.5 {
 302                 quads.push((x, y, w, h, color));
 303                 return;
 304             }
 305             
 306             quads.push((x + radius, y, w - 2.0 * radius, h, color));
 307             quads.push((x, y + radius, radius, h - 2.0 * radius, color));
 308             quads.push((x + w - radius, y + radius, radius, h - 2.0 * radius, color));
 309             
 310             let steps = radius.round() as i32;
 311             for i in 0..steps {
 312                 let dy = i as f32;
 313                 let next_dy = (i + 1) as f32;
 314                 
 315                 let cx = (radius * radius - (radius - dy) * (radius - dy)).sqrt();
 316                 let next_cx = (radius * radius - (radius - next_dy) * (radius - next_dy)).sqrt();
 317                 let avg_cx = (cx + next_cx) * 0.5;
 318                 
 319                 let strip_w = avg_cx;
 320                 let strip_h = 1.0f32;
 321                 
 322                 if strip_w > 0.0 {
 323                     quads.push((x + radius - strip_w, y + dy, strip_w, strip_h, color));
 324                     quads.push((x + w - radius, y + dy, strip_w, strip_h, color));
 325                     quads.push((x + radius - strip_w, y + h - dy - strip_h, strip_w, strip_h, color));
 326                     quads.push((x + w - radius, y + h - dy - strip_h, strip_w, strip_h, color));
 327                 }
 328             }
 329         };
 330 
 331         if self.recessed() {
 332             // The Breadcrumb composition: ONE well across the whole control,
 333             // the hex text on its floor at the left and the swatch as the
 334             // colour laid flush on the floor's right segment (the well's
 335             // rounded end is its own), the two parted by a seam groove. The
 336             // caret quad first (it rides on the floor), the carve after the
 337             // fills so the walls' shading falls over both, the seam last so it
 338             // dies into the well's rolled edge. Over a checker where the
 339             // colour carries alpha, so the transparency reads through.
 340             for (qx, qy, qw, qh, qc) in quads.drain(..) {
 341                 ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
 342             }
 343             let Some((wx, wy, ww, wh, radius, depth)) = self.field_relief(rect) else {
 344                 return;
 345             };
 346             let well = Rect { x: wx, y: wy, width: ww, height: wh };
 347             // The floor: past the wall's inner half-span (the ProgressBar's inset).
 348             let floor = Rect {
 349                 x: rect.x + depth,
 350                 y: rect.y + depth,
 351                 width: (rect.width - 2.0 * depth).max(0.0),
 352                 height: (visual_h - 2.0 * depth).max(0.0),
 353             };
 354             let seam_x = pick_x;
 355             let swatch = Rect { x: seam_x, y: floor.y, width: (floor.x + floor.width - seam_x).max(0.0), height: floor.height };
 356             let sr = crate::layout::textbox_corner_radius().min(swatch.height * 0.5);
 357             let right_end = (false, true, true, false);
 358             if self.with_alpha {
 359                 ctx.rounded_rect(swatch, sr, right_end, [0.8, 0.8, 0.8, 1.0]);
 360                 // The white cells, those at the right edge trimmed to the end's
 361                 // arc at their own row (the Breadcrumb's banded-wash sampling).
 362                 //
 363                 // Zero-radius rounded rects, NOT plain quads. The adapter's
 364                 // legacy plain-quad view (`own_plain_quads`) is this paint
 365                 // filtered to `Prim::Quad`, and a host that reads that view
 366                 // beside the paint — the designer, through ParametersBg's
 367                 // `plain_quads` — draws it AFTER the prims. As quads the
 368                 // cells came back a second time OVER the colour fill below,
 369                 // so every colour but white showed a checker as if it were
 370                 // transparent (invisible with the default white, 2026-09-21).
 371                 let grid = 6.0;
 372                 let cols = (swatch.width / grid).ceil() as i32;
 373                 let rows = (swatch.height / grid).ceil() as i32;
 374                 let arc = |yc: f32| -> f32 {
 375                     let dy = if yc < swatch.y + sr {
 376                         sr - (yc - swatch.y)
 377                     } else if yc > swatch.y + swatch.height - sr {
 378                         yc - (swatch.y + swatch.height - sr)
 379                     } else {
 380                         return 0.0;
 381                     };
 382                     sr - (sr * sr - dy * dy).max(0.0).sqrt()
 383                 };
 384                 for r in 0..rows {
 385                     for c in 0..cols {
 386                         if (r + c) % 2 == 1 {
 387                             let qx = swatch.x + c as f32 * grid;
 388                             let qy = swatch.y + r as f32 * grid;
 389                             let qh = grid.min(swatch.y + swatch.height - qy);
 390                             let right_edge = swatch.x + swatch.width - arc(qy + qh * 0.5);
 391                             let qw = grid.min(right_edge - qx);
 392                             if qw > 0.0 && qh > 0.0 {
 393                                 ctx.rounded_rect(
 394                                     Rect { x: qx, y: qy, width: qw, height: qh },
 395                                     0.0,
 396                                     (false, false, false, false),
 397                                     [1.0, 1.0, 1.0, 1.0],
 398                                 );
 399                             }
 400                         }
 401                     }
 402                 }
 403             }
 404             ctx.rounded_rect(swatch, sr, right_end, linear_c);
 405             let radii = (radius, radius, radius, radius);
 406             if self.editing {
 407                 let hc = crate::color::highlight_primary_color();
 408                 ctx.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
 409             } else {
 410                 ctx.recess(well, radii, depth);
 411             }
 412             ctx.groove(
 413                 (seam_x, well.y),
 414                 (seam_x, well.y + well.height),
 415                 crate::widget::Breadcrumb::SEAM_WIDTH,
 416                 depth,
 417                 well,
 418             );
 419             let hex = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
 420             // Bounded by the seam: the field is the well left of it, and while
 421             // `editing` this holds whatever has been typed, not a 7-character
 422             // hex code.
 423             ctx.text_with(
 424                 hex,
 425                 rect.x + crate::layout::CONTROL_TEXT_INSET,
 426                 crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0),
 427                 12.0,
 428                 [0xcc, 0xcc, 0xd4],
 429                 None,
 430                 Some([rect.x, rect.y, seam_x, rect.y + rect.height]),
 431             );
 432             return;
 433         }
 434 
 435         let steps = 6;
 436         for i in (1..=steps).rev() {
 437             let offset = i as f32 * 0.75;
 438             let rx = px - offset;
 439             let ry = py - offset;
 440             let rw = pw + 2.0 * offset;
 441             let rh = ph + 2.0 * offset;
 442             let alpha = 0.08 * (1.0 - (i as f32 / steps as f32).powf(1.5));
 443             if alpha > 0.001 {
 444                 add_rounded_rect(&mut quads, [r, g, b, alpha], rx, ry, rw, rh, preview_radius + offset);
 445             }
 446         }
 447 
 448         add_rounded_rect(&mut quads, border_c, px, py, pw, ph, preview_radius);
 449 
 450         if self.with_alpha {
 451             add_rounded_rect(
 452                 &mut quads,
 453                 [0.8, 0.8, 0.8, 1.0],
 454                 px + border_w,
 455                 py + border_w,
 456                 (pw - 2.0 * border_w).max(0.0),
 457                 (ph - 2.0 * border_w).max(0.0),
 458                 (preview_radius - border_w).max(0.0),
 459             );
 460             
 461             let grid_size = 6.0;
 462             let start_x = px + border_w;
 463             let start_y = py + border_w;
 464             let inner_w = (pw - 2.0 * border_w).max(0.0);
 465             let inner_h = (ph - 2.0 * border_w).max(0.0);
 466             
 467             let cols = (inner_w / grid_size).ceil() as i32;
 468             let rows = (inner_h / grid_size).ceil() as i32;
 469             for r in 0..rows {
 470                 for c in 0..cols {
 471                     if (r + c) % 2 == 1 {
 472                         let qx = start_x + c as f32 * grid_size;
 473                         let qy = start_y + r as f32 * grid_size;
 474                         let qw = grid_size.min(start_x + inner_w - qx);
 475                         let qh = grid_size.min(start_y + inner_h - qy);
 476                         if qw > 0.0 && qh > 0.0 {
 477                             quads.push((qx, qy, qw, qh, [1.0, 1.0, 1.0, 1.0]));
 478                         }
 479                     }
 480                 }
 481             }
 482         }
 483 
 484         add_rounded_rect(
 485             &mut quads,
 486             linear_c,
 487             px + border_w,
 488             py + border_w,
 489             (pw - 2.0 * border_w).max(0.0),
 490             (ph - 2.0 * border_w).max(0.0),
 491             (preview_radius - border_w).max(0.0),
 492         );
 493     
 494         for (qx, qy, qw, qh, qc) in quads {
 495             ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
 496         }
 497 
 498         let hex = if self.editing { self.edit_buffer.clone() } else { self.value_hex() };
 499         ctx.text_with(
 500             hex,
 501             rect.x + crate::layout::CONTROL_TEXT_INSET,
 502             crate::layout::align_text_y(rect.y, rect.height, 12.0, 0.0),
 503             12.0,
 504             [0xcc, 0xcc, 0xd4],
 505             None,
 506             Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]),
 507         );
 508     }
 509 }
 510 
 511 impl Input for ColorSelector {
 512     fn focus_role(&self) -> crate::widget::FocusRole {
 513         crate::widget::FocusRole::Well
 514     }
 515     fn opens_context_menu(&self) -> bool {
 516         true
 517     }
 518 
 519     fn take_click(&mut self) -> bool {
 520         if self.just_clicked { self.just_clicked = false; true } else { false }
 521     }
 522 
 523     fn take_change(&mut self) -> bool {
 524         let ret = self.just_changed;
 525         self.just_changed = false;
 526         ret
 527     }
 528 
 529     fn value_string(&self) -> Option<String> {
 530         Some(self.value_hex())
 531     }
 532 
 533     fn set_value_string(&mut self, val: &str) -> bool {
 534         if let Some(c) = parse_hex(val) {
 535             let target_color = [c[0], c[1], c[2]];
 536             let target_alpha = if self.with_alpha { c[3] } else { 255 };
 537             if self.color != target_color || (self.with_alpha && self.alpha != target_alpha) {
 538                 self.color = target_color;
 539                 self.alpha = target_alpha;
 540                 self.just_changed = true;
 541                 if self.editing {
 542                     self.edit_buffer = self.value_hex();
 543                     self.cursor_idx = self.edit_buffer.chars().count();
 544                 }
 545                 return true;
 546             }
 547         }
 548         false
 549     
 550     }
 551 
 552     fn wants_tick(&self) -> bool {
 553         true
 554     }
 555 
 556     fn tick(&mut self, _dt: f32, _rect: Rect) -> bool {
 557         // Apply streamed picker lines as they arrive — the color changes live
 558         // while the editor stays open. The picker's Apply prints a final line
 559         // (already applied here); Cancel prints `cancel`, restoring the value
 560         // the picker launched with.
 561         let mut redraw = false;
 562         if let Some(rx) = &self.live_rx {
 563             // A live picker session is activity: the runner sleeps between
 564             // ticks when nothing is animating, and these lines come from a
 565             // reader thread it cannot see, so keep the frame cadence for as
 566             // long as the picker is open.
 567             redraw = true;
 568             let mut lines = Vec::new();
 569             let mut disconnected = false;
 570             loop {
 571                 match rx.try_recv() {
 572                     Ok(line) => lines.push(line),
 573                     Err(std::sync::mpsc::TryRecvError::Empty) => break,
 574                     Err(std::sync::mpsc::TryRecvError::Disconnected) => {
 575                         disconnected = true;
 576                         break;
 577                     }
 578                 }
 579             }
 580             if disconnected {
 581                 self.live_rx = None;
 582             }
 583             for line in lines {
 584                 let line = line.trim().to_string();
 585                 let target = if line == "cancel" {
 586                     self.revert_hex.clone().and_then(|h| parse_hex(&h))
 587                 } else {
 588                     parse_hex(&line)
 589                 };
 590                 if let Some(c) = target {
 591                     let color = [c[0], c[1], c[2]];
 592                     let alpha = if self.with_alpha { c[3] } else { 255 };
 593                     if self.color != color || self.alpha != alpha {
 594                         self.color = color;
 595                         self.alpha = alpha;
 596                         self.just_changed = true;
 597                         redraw = true;
 598                     }
 599                 }
 600             }
 601         }
 602         let mut child_opt = self.child.lock().unwrap();
 603         if let Some(ref mut child) = *child_opt {
 604             match child.try_wait() {
 605                 Ok(Some(_status)) => {
 606                     // The reader thread owns stdout and has already forwarded
 607                     // every line (including Apply's final one) — just reap.
 608                     *child_opt = None;
 609                 }
 610                 Ok(None) => {}
 611                 Err(e) => {
 612                     log::error!("Error checking color selector child process: {:?}", e);
 613                     *child_opt = None;
 614                 }
 615             }
 616         }
 617         redraw
 618     }
 619 
 620     fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
 621         match event {
 622             Event::MouseButton { button, state, x, y, .. } => {
 623                 if *button != MouseButton::Left {
 624                     return false;
 625                 }
 626                 if *state != ElementState::Pressed {
 627                     return false;
 628                 }
 629                 let (px, py) = (*x, *y);
 630                 let _ = py;
 631                 let rect = ectx.rect;
 632                 if px >= rect.x + rect.width * 0.65 {
 633             let hex = self.value_hex();
 634             let mut child_guard = self.child.lock().unwrap();
 635             if let Some(mut old_child) = child_guard.take() {
 636                 let _ = old_child.kill();
 637             }
 638 
 639             let cmd_path = if let Ok(mut exe_path) = std::env::current_exe() {
 640                 exe_path.pop(); // remove executable name
 641                 let local_path = exe_path.join(&self.command);
 642                 if local_path.exists() {
 643                     local_path.to_string_lossy().into_owned()
 644                 } else {
 645                     let home = std::env::var("HOME").unwrap_or_default();
 646                     let local_bin = std::path::Path::new(&home).join(".local/bin").join(&self.command);
 647                     if local_bin.exists() {
 648                         local_bin.to_string_lossy().into_owned()
 649                     } else {
 650                         self.command.clone()
 651                     }
 652                 }
 653             } else {
 654                 self.command.clone()
 655             };
 656 
 657             // Ask the compositor to open the picker at this control instead of
 658             // its remembered position: the pointer is on the swatch right now,
 659             // so its location IS the control's location. One-shot, best-effort
 660             // (`place-next` consumed at the picker's map; ignored off-cce).
 661             if let Ok(reply) = crate::ipc::send_command("cce", "pointer-location") {
 662                 let mut px = None;
 663                 let mut py = None;
 664                 for tok in reply.split_whitespace() {
 665                     if let Some(v) = tok.strip_prefix("x=") {
 666                         px = v.parse::<f64>().ok();
 667                     } else if let Some(v) = tok.strip_prefix("y=") {
 668                         py = v.parse::<f64>().ok();
 669                     }
 670                 }
 671                 if let (Some(x), Some(y)) = (px, py) {
 672                     let app_id = std::path::Path::new(&self.command)
 673                         .file_name()
 674                         .map(|n| n.to_string_lossy().into_owned())
 675                         .unwrap_or_else(|| self.command.clone());
 676                     let _ = crate::ipc::send_command(
 677                         "cce",
 678                         &format!("place-next {} {:.0} {:.0}", app_id, x, y),
 679                     );
 680                 }
 681             }
 682 
 683             let mut cmd = std::process::Command::new(&cmd_path);
 684             cmd.arg(&hex);
 685             if self.with_alpha {
 686                 cmd.arg("--alpha");
 687             }
 688             // Live picking: the picker streams every change on stdout; a
 689             // reader thread forwards lines so `tick` applies them while the
 690             // editor stays open. `cancel` restores the launch value.
 691             cmd.arg("--stream");
 692             if let Ok(mut child) = cmd.stdout(std::process::Stdio::piped()).spawn() {
 693                 if let Some(stdout) = child.stdout.take() {
 694                     let (tx, rx) = std::sync::mpsc::channel::<String>();
 695                     std::thread::spawn(move || {
 696                         use std::io::BufRead;
 697                         let reader = std::io::BufReader::new(stdout);
 698                         for line in reader.lines().map_while(Result::ok) {
 699                             if tx.send(line).is_err() {
 700                                 break;
 701                             }
 702                         }
 703                     });
 704                     self.live_rx = Some(rx);
 705                     self.revert_hex = Some(hex.clone());
 706                 }
 707                 *child_guard = Some(child);
 708             }
 709                     return true;
 710                 }
 711                 ectx.request_focus();
 712                 true
 713             }
 714             Event::KeyInput(event) => {
 715                 if event.state != ElementState::Pressed {
 716                     return false;
 717                 }
 718                 if !self.editing {
 719                     return false;
 720                 }
 721 
 722 
 723         
 724         let mut state = TextEditorState {
 725             buffer: self.edit_buffer.clone(),
 726             cursor_idx: self.cursor_idx,
 727             select_anchor: None,
 728             all_selected: false,
 729         };
 730         
 731         let mut handled = false;
 732         match &event.logical_key {
 733             Key::Named(NamedKey::Backspace) => {
 734                 state.delete_backwards();
 735                 handled = true;
 736             }
 737             Key::Named(NamedKey::Delete) => {
 738                 state.delete_forwards();
 739                 handled = true;
 740             }
 741             Key::Named(NamedKey::ArrowLeft) => {
 742                 state.move_cursor_left(false);
 743                 handled = true;
 744             }
 745             Key::Named(NamedKey::ArrowRight) => {
 746                 state.move_cursor_right(false);
 747                 handled = true;
 748             }
 749             Key::Named(NamedKey::Enter) => {
 750                 if let Some(c) = parse_hex(&state.buffer) {
 751                     self.color = [c[0], c[1], c[2]];
 752                     self.alpha = if self.with_alpha { c[3] } else { 255 };
 753                 }
 754                 self.editing = false;
 755                 handled = true;
 756             }
 757             Key::Named(NamedKey::Escape) => {
 758                 self.editing = false;
 759                 handled = true;
 760             }
 761             _ => {
 762                 if let Some(text) = &event.text {
 763                     for ch in text.chars() {
 764                         match ch {
 765                             '#' => {
 766                                 if state.buffer.is_empty() {
 767                                     state.insert_text("#");
 768                                     handled = true;
 769                                 } else if state.cursor_idx == 0 && !state.buffer.starts_with('#') {
 770                                     state.insert_text("#");
 771                                     handled = true;
 772                                 }
 773                             }
 774                             '0'..='9' | 'a'..='f' | 'A'..='F' => {
 775                                 let count = state.buffer.chars().count();
 776                                 let max_len = if state.buffer.starts_with('#') {
 777                                     if self.with_alpha { 9 } else { 7 }
 778                                 } else {
 779                                     if self.with_alpha { 8 } else { 6 }
 780                                 };
 781                                 if count < max_len {
 782                                     state.insert_text(&ch.to_ascii_lowercase().to_string());
 783                                     handled = true;
 784                                 }
 785                             }
 786                             _ => {}
 787                         }
 788                     }
 789                 }
 790             }
 791         }
 792         
 793         if self.editing {
 794             self.edit_buffer = state.buffer;
 795             self.cursor_idx = state.cursor_idx;
 796         }
 797         handled
 798     
 799             }
 800             Event::MouseEnter => {
 801                 self.hovered = true;
 802                 false
 803             }
 804             Event::MouseLeave => {
 805                 self.hovered = false;
 806                 false
 807             }
 808             Event::FocusIn => {
 809                 self.begin_edit();
 810                 false
 811             }
 812             Event::FocusOut => {
 813                 if self.editing {
 814                     self.editing = false;
 815                     if let Some(c) = parse_hex(&self.edit_buffer) {
 816                         self.color = [c[0], c[1], c[2]];
 817                         self.alpha = if self.with_alpha { c[3] } else { 255 };
 818                     }
 819                 }
 820                 false
 821             }
 822             _ => false,
 823         }
 824     }
 825 }
 826 
 827 fn parse_hex(s: &str) -> Option<[u8; 4]> {
 828     crate::color::parse_hex_bytes(s)
 829 }
 830 
 831 #[cfg(test)]
 832 mod tests {
 833     use super::*;
 834 
 835     #[test]
 836     fn test_colorselector_keyboard_navigation() {
 837         let mut dummy = crate::context::UiContext::new();
 838         let mut cs = ColorSelector::new([255, 0, 0]);
 839         assert_eq!(cs.color, [255, 0, 0]);
 840         assert_eq!(cs.alpha, 255);
 841         assert!(!cs.editing);
 842 
 843         // 1. Focus color selector
 844         cs.focus();
 845         assert!(cs.editing);
 846         assert_eq!(cs.edit_buffer, "#ff0000");
 847         assert_eq!(cs.cursor_idx, 7);
 848 
 849         // 2. Backspace deletes character before cursor
 850         let backspace_ev = KeyEvent {
 851             state: ElementState::Pressed,
 852             logical_key: Key::Named(NamedKey::Backspace),
 853             text: None,
 854             repeat: false,
 855             ctrl: false,
 856             shift: false,
 857             alt: false,
 858         };
 859         let handled = cs.keyboard_input(&backspace_ev, &mut dummy);
 860         assert!(handled);
 861         assert_eq!(cs.edit_buffer, "#ff000");
 862         assert_eq!(cs.cursor_idx, 6);
 863 
 864         // 3. ArrowLeft moves cursor index
 865         let left_ev = KeyEvent {
 866             state: ElementState::Pressed,
 867             logical_key: Key::Named(NamedKey::ArrowLeft),
 868             text: None,
 869             repeat: false,
 870             ctrl: false,
 871             shift: false,
 872             alt: false,
 873         };
 874         let handled = cs.keyboard_input(&left_ev, &mut dummy);
 875         assert!(handled);
 876         assert_eq!(cs.cursor_idx, 5);
 877 
 878         // 4. Backspace at cursor index 5
 879         let handled = cs.keyboard_input(&backspace_ev, &mut dummy);
 880         assert!(handled);
 881         assert_eq!(cs.edit_buffer, "#ff00");
 882         assert_eq!(cs.cursor_idx, 4);
 883 
 884         // 5. ArrowRight moves cursor index
 885         let right_ev = KeyEvent {
 886             state: ElementState::Pressed,
 887             logical_key: Key::Named(NamedKey::ArrowRight),
 888             text: None,
 889             repeat: false,
 890             ctrl: false,
 891             shift: false,
 892             alt: false,
 893         };
 894         let handled = cs.keyboard_input(&right_ev, &mut dummy);
 895         assert!(handled);
 896         assert_eq!(cs.cursor_idx, 5);
 897 
 898         // 6. Delete deletes character at cursor
 899         let delete_ev = KeyEvent {
 900             state: ElementState::Pressed,
 901             logical_key: Key::Named(NamedKey::Delete),
 902             text: None,
 903             repeat: false,
 904             ctrl: false,
 905             shift: false,
 906             alt: false,
 907         };
 908         // Move cursor to index 3
 909         let handled = cs.keyboard_input(&left_ev, &mut dummy); // 4
 910         assert!(handled);
 911         let handled = cs.keyboard_input(&left_ev, &mut dummy); // 3
 912         assert!(handled);
 913         assert_eq!(cs.cursor_idx, 3);
 914         // buffer is "#ff0", index 3 points to the first '0'. Let's delete it.
 915         let handled = cs.keyboard_input(&delete_ev, &mut dummy);
 916         assert!(handled);
 917         assert_eq!(cs.edit_buffer, "#ff0");
 918         assert_eq!(cs.cursor_idx, 3);
 919 
 920         // 7. Typing hex character inserts at cursor index
 921         let type_b_ev = KeyEvent {
 922             state: ElementState::Pressed,
 923             logical_key: Key::Character("b".to_string()),
 924             text: Some("b".to_string()),
 925             repeat: false,
 926             ctrl: false,
 927             shift: false,
 928             alt: false,
 929         };
 930         let handled = cs.keyboard_input(&type_b_ev, &mut dummy);
 931         assert!(handled);
 932         assert_eq!(cs.edit_buffer, "#ffb0");
 933         assert_eq!(cs.cursor_idx, 4);
 934 
 935         // Move to index 1 and insert a 'c'
 936         for _ in 0..3 {
 937             let handled = cs.keyboard_input(&left_ev, &mut dummy);
 938             assert!(handled);
 939         }
 940         assert_eq!(cs.cursor_idx, 1);
 941         let type_c_ev = KeyEvent {
 942             state: ElementState::Pressed,
 943             logical_key: Key::Character("c".to_string()),
 944             text: Some("c".to_string()),
 945             repeat: false,
 946             ctrl: false,
 947             shift: false,
 948             alt: false,
 949         };
 950         let handled = cs.keyboard_input(&type_c_ev, &mut dummy);
 951         assert!(handled);
 952         assert_eq!(cs.edit_buffer, "#cffb0");
 953         assert_eq!(cs.cursor_idx, 2);
 954 
 955         // 8. Enter commits the color
 956         let type_5_ev = KeyEvent {
 957             state: ElementState::Pressed,
 958             logical_key: Key::Character("5".to_string()),
 959             text: Some("5".to_string()),
 960             repeat: false,
 961             ctrl: false,
 962             shift: false,
 963             alt: false,
 964         };
 965         // Move to index 5
 966         for _ in 0..4 {
 967             cs.keyboard_input(&right_ev, &mut dummy);
 968         }
 969         assert_eq!(cs.cursor_idx, 6);
 970         cs.keyboard_input(&type_5_ev, &mut dummy);
 971         assert_eq!(cs.edit_buffer, "#cffb05");
 972         assert_eq!(cs.cursor_idx, 7);
 973 
 974         let enter_ev = KeyEvent {
 975             state: ElementState::Pressed,
 976             logical_key: Key::Named(NamedKey::Enter),
 977             text: None,
 978             repeat: false,
 979             ctrl: false,
 980             shift: false,
 981             alt: false,
 982         };
 983         let handled = cs.keyboard_input(&enter_ev, &mut dummy);
 984         assert!(handled);
 985         assert!(!cs.editing);
 986         assert_eq!(cs.color, [0xcf, 0xfb, 0x05]);
 987         assert_eq!(cs.alpha, 255);
 988     }
 989 
 990     #[test]
 991     fn test_colorselector_alpha_support() {
 992         let mut dummy = crate::context::UiContext::new();
 993         let mut cs = ColorSelector::new_rgba([255, 0, 0, 128]);
 994         assert_eq!(cs.color, [255, 0, 0]);
 995         assert_eq!(cs.alpha, 128);
 996         assert!(cs.with_alpha);
 997 
 998         cs.focus();
 999         assert!(cs.editing);
1000         assert_eq!(cs.edit_buffer, "#ff000080");
1001         assert_eq!(cs.cursor_idx, 9);
1002 
1003         let type_a_ev = KeyEvent {
1004             state: ElementState::Pressed,
1005             logical_key: Key::Character("a".to_string()),
1006             text: Some("a".to_string()),
1007             repeat: false,
1008             ctrl: false,
1009             shift: false,
1010             alt: false,
1011         };
1012         let backspace_ev = KeyEvent {
1013             state: ElementState::Pressed,
1014             logical_key: Key::Named(NamedKey::Backspace),
1015             text: None,
1016             repeat: false,
1017             ctrl: false,
1018             shift: false,
1019             alt: false,
1020         };
1021         cs.keyboard_input(&backspace_ev, &mut dummy);
1022         cs.keyboard_input(&backspace_ev, &mut dummy);
1023         assert_eq!(cs.edit_buffer, "#ff0000");
1024 
1025         let type_b_ev = KeyEvent {
1026             state: ElementState::Pressed,
1027             logical_key: Key::Character("b".to_string()),
1028             text: Some("b".to_string()),
1029             repeat: false,
1030             ctrl: false,
1031             shift: false,
1032             alt: false,
1033         };
1034         cs.keyboard_input(&type_a_ev, &mut dummy);
1035         cs.keyboard_input(&type_b_ev, &mut dummy);
1036         assert_eq!(cs.edit_buffer, "#ff0000ab");
1037 
1038         let enter_ev = KeyEvent {
1039             state: ElementState::Pressed,
1040             logical_key: Key::Named(NamedKey::Enter),
1041             text: None,
1042             repeat: false,
1043             ctrl: false,
1044             shift: false,
1045             alt: false,
1046         };
1047         cs.keyboard_input(&enter_ev, &mut dummy);
1048         assert!(!cs.editing);
1049         assert_eq!(cs.color, [255, 0, 0]);
1050         assert_eq!(cs.alpha, 171);
1051     }
1052     /// The alpha preview's checker must not reach the legacy plain-quad
1053     /// view: a host that draws that view beside the paint draws it AFTER the
1054     /// prims, and the white cells then landed over the colour fill — a black
1055     /// swatch read as a black-and-white checker. The paint keeps its order
1056     /// (base, cells, colour on top) and the plain view carries no cells.
1057     #[test]
1058     fn alpha_checker_stays_out_of_the_plain_quad_view() {
1059         use crate::scene::paint::Prim;
1060         let mut cs = ColorSelector::new_rgba([0, 0, 0, 255]);
1061         cs.recessed = Some(true);
1062         let rect = Rect { x: 0.0, y: 0.0, width: 200.0, height: 40.0 };
1063         crate::widget::WidgetHost::set_rect(&mut cs, rect.x, rect.y, rect.width, rect.height);
1064         let mut pc = PaintCtx::new();
1065         let inner: &ColorSelector = &cs;
1066         crate::widget::Paint::paint(inner, rect, &mut pc);
1067         let items = pc.finish().items;
1068         let last_fill = items
1069             .iter()
1070             .rposition(|it| matches!(it.prim, Prim::RoundedRect { color, .. } if color == [0.0, 0.0, 0.0, 1.0]))
1071             .expect("the colour fill is painted");
1072         let white_cells: Vec<usize> = items
1073             .iter()
1074             .enumerate()
1075             .filter(|(_, it)| matches!(it.prim, Prim::RoundedRect { color, radius, .. } if color == [1.0; 4] && radius == 0.0))
1076             .map(|(i, _)| i)
1077             .collect();
1078         assert!(!white_cells.is_empty(), "the checker is painted");
1079         assert!(white_cells.iter().all(|&i| i < last_fill), "every cell is under the colour fill");
1080         assert!(
1081             !items.iter().any(|it| matches!(it.prim, Prim::Quad { color, .. } if color == [1.0; 4])),
1082             "no cell is a plain quad, so the legacy plain view cannot carry it"
1083         );
1084         let plain = crate::widget::WidgetHost::extra_quads(&cs);
1085         assert!(plain.iter().all(|q| q.4 != [1.0, 1.0, 1.0, 1.0]), "the plain view has no white cells: {plain:?}");
1086     }
1087 
1088 }
1089 
1090