git.lucas.co / cce-color-editor
color picker and palette editor
git clone https://git.lucas.co/cce-color-editor.git

commit9e3b3ba786459cc6067512fa1f77aef3617fd776
parent2d8f1d9044
authorLucas Galante <[email protected]>
date2026-06-13 21:29
Refactor color interface and update widgets/rendering logic

 src/main.rs | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 177 insertions(+), 36 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 2908c23..66ff68c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -95,14 +95,22 @@ fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
     buf
 }
 
-fn parse_hex(hex: &str) -> Option<(f32, f32, f32)> {
+fn parse_hex(hex: &str) -> Option<(f32, f32, f32, Option<f32>)> {
     let s = hex.trim_start_matches('#');
     if s.len() == 6 {
         u32::from_str_radix(s, 16).ok().map(|v| {
             let r = ((v >> 16) & 0xFF) as f32 / 255.0;
             let g = ((v >> 8) & 0xFF) as f32 / 255.0;
             let b = (v & 0xFF) as f32 / 255.0;
-            (r, g, b)
+            (r, g, b, None)
+        })
+    } else if s.len() == 8 {
+        u32::from_str_radix(s, 16).ok().map(|v| {
+            let r = ((v >> 24) & 0xFF) as f32 / 255.0;
+            let g = ((v >> 16) & 0xFF) as f32 / 255.0;
+            let b = ((v >> 8) & 0xFF) as f32 / 255.0;
+            let a = (v & 0xFF) as f32 / 255.0;
+            (r, g, b, Some(a))
         })
     } else {
         None
@@ -194,7 +202,7 @@ struct TextItem {
 }
 
 #[derive(Clone, Copy, PartialEq)]
-enum DragTarget { Red, Green, Blue, Hue, Saturation, Lightness }
+enum DragTarget { Red, Green, Blue, Hue, Saturation, Lightness, Alpha }
 
 #[derive(Clone, Copy, PartialEq)]
 enum Action { Apply, Cancel }
@@ -221,6 +229,8 @@ struct ColorApp {
     hue: f32,
     saturation: f32,
     lightness: f32,
+    alpha: f32,
+    with_alpha: bool,
 
     font_system: FontSystem,
     swash_cache: SwashCache,
@@ -253,6 +263,8 @@ impl ColorApp {
         red: f32,
         green: f32,
         blue: f32,
+        alpha: f32,
+        with_alpha: bool,
         scale: f64,
     ) -> Self {
         let surface = compositor_state.create_surface(qh);
@@ -260,7 +272,8 @@ impl ColorApp {
         let window = xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, qh);
         window.set_title("Clear Color Interface");
         window.set_app_id("cce-color-interface");
-        window.set_min_size(Some((WIN_W as u32, WIN_H as u32)));
+        let win_h = if with_alpha { WIN_H + SLIDER_ROW_H } else { WIN_H };
+        window.set_min_size(Some((WIN_W as u32, win_h as u32)));
         window.commit();
 
         let wayland_handle = Box::leak(Box::new(clear_ui::wayland::WaylandSurfaceHandle {
@@ -287,7 +300,7 @@ impl ColorApp {
 
         let scale_factor = scale;
         let width = (WIN_W * scale_factor as f32) as u32;
-        let height = (WIN_H * scale_factor as f32) as u32;
+        let height = (win_h * scale_factor as f32) as u32;
 
         let mut config = wgpu_surface.get_default_config(&adapter, width, height).expect("config");
         config.width = width;
@@ -359,6 +372,8 @@ impl ColorApp {
             vertex_buffer, vertex_count: 0,
             red, green, blue,
             hue, saturation, lightness,
+            alpha,
+            with_alpha,
             font_system, swash_cache, text_atlas, text_renderer, text_viewport,
             rects: Vec::new(),
             gradient_rects: Vec::new(),
@@ -376,10 +391,18 @@ impl ColorApp {
     }
 
     fn hex(&self) -> String {
-        format!("#{:02X}{:02X}{:02X}",
-            (self.red * 255.0) as u8,
-            (self.green * 255.0) as u8,
-            (self.blue * 255.0) as u8)
+        if self.with_alpha {
+            format!("#{:02X}{:02X}{:02X}{:02X}",
+                (self.red * 255.0) as u8,
+                (self.green * 255.0) as u8,
+                (self.blue * 255.0) as u8,
+                (self.alpha * 255.0) as u8)
+        } else {
+            format!("#{:02X}{:02X}{:02X}",
+                (self.red * 255.0) as u8,
+                (self.green * 255.0) as u8,
+                (self.blue * 255.0) as u8)
+        }
     }
 
     fn rebuild_layout(&mut self, sw: f32, sh: f32) {
@@ -404,8 +427,12 @@ impl ColorApp {
             color: color::CONTENT_BG,
         });
 
-        let channels = [self.red, self.green, self.blue, self.hue, self.saturation, self.lightness];
-        let labels = ['R', 'G', 'B', 'H', 'S', 'L'];
+        let mut channels = vec![self.red, self.green, self.blue, self.hue, self.saturation, self.lightness];
+        let mut labels = vec!['R', 'G', 'B', 'H', 'S', 'L'];
+        if self.with_alpha {
+            channels.push(self.alpha);
+            labels.push('A');
+        }
 
         let red = self.red;
         let green = self.green;
@@ -413,28 +440,30 @@ impl ColorApp {
         let hue = self.hue;
         let saturation = self.saturation;
         let lightness = self.lightness;
+        let alpha = self.alpha;
 
         let get_color_at = |i: usize, t: f32| -> [f32; 4] {
             match i {
-                0 => [t, green, blue, 1.0],
-                1 => [red, t, blue, 1.0],
-                2 => [red, green, t, 1.0],
+                0 => [t, green, blue, alpha],
+                1 => [red, t, blue, alpha],
+                2 => [red, green, t, alpha],
                 3 => {
                     let (r, g, b) = hsl_to_rgb(t, saturation, lightness);
-                    [r, g, b, 1.0]
+                    [r, g, b, alpha]
                 }
                 4 => {
                     let (r, g, b) = hsl_to_rgb(hue, t, lightness);
-                    [r, g, b, 1.0]
+                    [r, g, b, alpha]
                 }
-                _ => {
+                5 => {
                     let (r, g, b) = hsl_to_rgb(hue, saturation, t);
-                    [r, g, b, 1.0]
+                    [r, g, b, alpha]
                 }
+                _ => [red, green, blue, t],
             }
         };
 
-        for i in 0..6 {
+        for i in 0..channels.len() {
             let row_y = (SLIDER_START_Y + i as f32 * SLIDER_ROW_H) * s;
             let track_y = row_y + ((SLIDER_ROW_H - SLIDER_TRACK_H) / 2.0) * s;
 
@@ -447,6 +476,44 @@ impl ColorApp {
                 color: [0.08, 0.08, 0.10, 1.0],
             });
 
+            if i == 6 {
+                // Draw checkerboard behind the alpha slider track
+                let track_x = SLIDER_TRACK_X * s;
+                let track_w = SLIDER_TRACK_W * s;
+                let track_h = SLIDER_TRACK_H * s;
+                let grid_size = track_h / 2.0; // Two rows of checkers
+                let cols = (track_w / grid_size).ceil() as i32;
+                
+                // First draw a solid light gray background
+                rects.push(RectWidget {
+                    x: track_x,
+                    y: track_y,
+                    w: track_w,
+                    h: track_h,
+                    color: [0.8, 0.8, 0.8, 1.0],
+                });
+                
+                for r in 0..2 {
+                    for c in 0..cols {
+                        if (r + c) % 2 == 1 {
+                            let qx = track_x + c as f32 * grid_size;
+                            let qy = track_y + r as f32 * grid_size;
+                            let qw = grid_size.min(track_x + track_w - qx);
+                            let qh = grid_size.min(track_y + track_h - qy);
+                            if qw > 0.0 && qh > 0.0 {
+                                rects.push(RectWidget {
+                                    x: qx,
+                                    y: qy,
+                                    w: qw,
+                                    h: qh,
+                                    color: [1.0, 1.0, 1.0, 1.0],
+                                });
+                            }
+                        }
+                    }
+                }
+            }
+
             // Draw gradient track
             let n_segments = if i == 3 { 30 } else { 10 };
             for j in 0..n_segments {
@@ -498,8 +565,10 @@ impl ColorApp {
                 format!("{}", (channels[i] * 255.0) as u8)
             } else if i == 3 {
                 format!("{}°", (channels[i] * 360.0).round() as u16)
-            } else {
+            } else if i < 6 {
                 format!("{}%", (channels[i] * 100.0).round() as u8)
+            } else {
+                format!("{}", (channels[i] * 255.0).round() as u8)
             };
             text_items.push(TextItem {
                 buffer: make_text_buffer(&mut self.font_system, &val, 11.0 * s),
@@ -508,24 +577,62 @@ impl ColorApp {
             });
         }
 
+        let preview_y_offset = if self.with_alpha { SLIDER_ROW_H } else { 0.0 };
+        let preview_y = PREVIEW_Y + preview_y_offset;
+        let button_y = BUTTON_Y + preview_y_offset;
+
+        if self.with_alpha {
+            // Draw checkerboard behind the preview box
+            let px = PREVIEW_X * s;
+            let py = preview_y * s;
+            let pw = PREVIEW_W * s;
+            let ph = PREVIEW_H * s;
+            
+            // Draw base light gray
+            rects.push(RectWidget {
+                x: px, y: py, w: pw, h: ph,
+                color: [0.8, 0.8, 0.8, 1.0],
+            });
+            
+            let grid_size = 12.0 * s;
+            let cols = (pw / grid_size).ceil() as i32;
+            let rows = (ph / grid_size).ceil() as i32;
+            for r in 0..rows {
+                for c in 0..cols {
+                    if (r + c) % 2 == 1 {
+                        let qx = px + c as f32 * grid_size;
+                        let qy = py + r as f32 * grid_size;
+                        let qw = grid_size.min(px + pw - qx);
+                        let qh = grid_size.min(py + ph - qy);
+                        if qw > 0.0 && qh > 0.0 {
+                            rects.push(RectWidget {
+                                x: qx, y: qy, w: qw, h: qh,
+                                color: [1.0, 1.0, 1.0, 1.0],
+                            });
+                        }
+                    }
+                }
+            }
+        }
+
         rects.push(RectWidget {
-            x: PREVIEW_X * s, y: PREVIEW_Y * s,
+            x: PREVIEW_X * s, y: preview_y * s,
             w: PREVIEW_W * s, h: PREVIEW_H * s,
-            color: color::to_linear([self.red, self.green, self.blue, 1.0]),
+            color: color::to_linear([self.red, self.green, self.blue, if self.with_alpha { self.alpha } else { 1.0 }]),
         });
 
         let hex = self.hex();
         text_items.push(TextItem {
             buffer: make_text_buffer(&mut self.font_system, &hex, 16.0 * s),
             x: (PREVIEW_X + PREVIEW_W + 16.0) * s,
-            y: (PREVIEW_Y + 26.0) * s,
+            y: (preview_y + 26.0) * s,
             color: glyphon::Color::rgb(0xe0, 0xe0, 0xe8),
         });
 
         // Apply button
         let apply_x = PREVIEW_X;
         let cancel_x = PREVIEW_X + BUTTON_W + BUTTON_GAP;
-        let btn_y = BUTTON_Y;
+        let btn_y = button_y;
         let btn_bg = [0.20, 0.40, 0.65, 1.0];
         let cancel_bg = [0.40, 0.20, 0.20, 1.0];
 
@@ -636,6 +743,7 @@ impl ColorApp {
                 DragTarget::Hue => 3,
                 DragTarget::Saturation => 4,
                 DragTarget::Lightness => 5,
+                DragTarget::Alpha => 6,
             };
             let s = self.scale_factor as f32;
             let (tx, _, tw, _) = Self::slider_physical_rect(i, s);
@@ -646,7 +754,8 @@ impl ColorApp {
                 2 => self.blue,
                 3 => self.hue,
                 4 => self.saturation,
-                _ => self.lightness,
+                5 => self.lightness,
+                _ => self.alpha,
             };
             if (new_val - old).abs() > 0.002 {
                 match i {
@@ -691,13 +800,16 @@ impl ColorApp {
                         self.green = g;
                         self.blue = b;
                     }
-                    _ => {
+                    5 => {
                         self.lightness = new_val;
                         let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
                         self.red = r;
                         self.green = g;
                         self.blue = b;
                     }
+                    _ => {
+                        self.alpha = new_val;
+                    }
                 }
                 self.needs_rebuild = true;
             }
@@ -709,7 +821,8 @@ impl ColorApp {
             clear_ui::widget::ElementState::Pressed => {
                 let s = self.scale_factor as f32;
                 let (px, py) = (self.cursor_x, self.cursor_y);
-                for i in 0..6 {
+                let num_sliders = if self.with_alpha { 7 } else { 6 };
+                for i in 0..num_sliders {
                     let (tx, ty, tw, th) = Self::slider_physical_rect(i, s);
                     if px >= tx && px <= tx + tw && py >= ty && py <= ty + th {
                         let val = ((px - tx) / tw).clamp(0.0, 1.0);
@@ -760,7 +873,7 @@ impl ColorApp {
                                 self.blue = b;
                                 self.dragging = Some(DragTarget::Saturation);
                             }
-                            _ => {
+                            5 => {
                                 self.lightness = val;
                                 let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
                                 self.red = r;
@@ -768,6 +881,10 @@ impl ColorApp {
                                 self.blue = b;
                                 self.dragging = Some(DragTarget::Lightness);
                             }
+                            _ => {
+                                self.alpha = val;
+                                self.dragging = Some(DragTarget::Alpha);
+                            }
                         }
                         self.needs_rebuild = true;
                         return;
@@ -794,7 +911,8 @@ impl ColorApp {
         let (px, py) = (self.cursor_x, self.cursor_y);
         let scroll_amount = scroll_amount_y;
         if scroll_amount.abs() > 0.0001 {
-            for i in 0..6 {
+            let num_sliders = if self.with_alpha { 7 } else { 6 };
+            for i in 0..num_sliders {
                 let (tx, ty, tw, th) = Self::slider_physical_rect(i, s);
                 if px >= tx && px <= tx + tw && py >= ty - 4.0 * s && py <= ty + th + 4.0 * s {
                     let step = 0.02;
@@ -804,7 +922,8 @@ impl ColorApp {
                         2 => self.blue,
                         3 => self.hue,
                         4 => self.saturation,
-                        _ => self.lightness,
+                        5 => self.lightness,
+                        _ => self.alpha,
                     };
                     let new_val = (old_val + scroll_amount * step).clamp(0.0, 1.0);
                     if (new_val - old_val).abs() > 0.0001 {
@@ -850,13 +969,16 @@ impl ColorApp {
                                 self.green = g;
                                 self.blue = b;
                             }
-                            _ => {
+                            5 => {
                                 self.lightness = new_val;
                                 let (r, g, b) = hsl_to_rgb(self.hue, self.saturation, self.lightness);
                                 self.red = r;
                                 self.green = g;
                                 self.blue = b;
                             }
+                            _ => {
+                                self.alpha = new_val;
+                            }
                         }
                         self.needs_rebuild = true;
                     }
@@ -961,7 +1083,8 @@ impl CompositorHandler for AppState {
         _surface.set_buffer_scale(scale_factor);
         if let Some(state) = &mut self.state {
             state.scale_factor = scale_factor as f64;
-            state.resize((WIN_W * state.scale_factor as f32) as u32, (WIN_H * state.scale_factor as f32) as u32);
+            let win_h = if state.with_alpha { WIN_H + SLIDER_ROW_H } else { WIN_H };
+            state.resize((WIN_W * state.scale_factor as f32) as u32, (win_h * state.scale_factor as f32) as u32);
             self.redraw = true;
         }
     }
@@ -1246,10 +1369,27 @@ delegate_output!(AppState);
 
 fn main() {
     let args: Vec<String> = std::env::args().collect();
-    let (r, g, b) = if args.len() > 1 {
-        parse_hex(&args[1]).unwrap_or((0.5, 0.5, 0.5))
+    let mut with_alpha = false;
+    let mut hex_arg = None;
+    for arg in args.iter().skip(1) {
+        if arg == "--alpha" || arg == "-a" {
+            with_alpha = true;
+        } else {
+            hex_arg = Some(arg.as_str());
+        }
+    }
+
+    let (r, g, b, a) = if let Some(hex) = hex_arg {
+        if let Some((r_parsed, g_parsed, b_parsed, parsed_a)) = parse_hex(hex) {
+            if parsed_a.is_some() {
+                with_alpha = true;
+            }
+            (r_parsed, g_parsed, b_parsed, parsed_a.unwrap_or(1.0))
+        } else {
+            (0.5, 0.5, 0.5, 1.0)
+        }
     } else {
-        (0.5, 0.5, 0.5)
+        (0.5, 0.5, 0.5, 1.0)
     };
 
     let conn = Connection::connect_to_env().unwrap();
@@ -1289,7 +1429,8 @@ fn main() {
         &qh,
         &app.compositor_state,
         &app.xdg_shell_state,
-        r, g, b,
+        r, g, b, a,
+        with_alpha,
         scale,
     ));