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

commitb7821bfadb4e9315d173ac0c59eb23a878f80e8c
parenta802e731a5
authorLucas Galante <[email protected]>
date2026-05-21 19:11
Add Apply/Cancel buttons and support parsing initial hex color from arguments

 src/main.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 120 insertions(+), 11 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index ba4acd5..9dc3975 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
 use winit::window::{Window, WindowAttributes};
 
 use clear_ui::color;
+use winit::platform::wayland::WindowAttributesExtWayland;
 use glyphon::{
     Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
     TextBounds, TextRenderer, Viewport,
@@ -56,6 +57,20 @@ fn make_text_buffer(fs: &mut FontSystem, text: &str, size: f32) -> Buffer {
     buf
 }
 
+fn parse_hex(hex: &str) -> Option<(f32, f32, 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)
+        })
+    } else {
+        None
+    }
+}
+
 const HEADER_H: f32 = 36.0;
 const SLIDER_ROW_H: f32 = 36.0;
 const SLIDER_START_Y: f32 = 48.0;
@@ -68,8 +83,12 @@ const PREVIEW_X: f32 = 12.0;
 const PREVIEW_Y: f32 = 168.0;
 const PREVIEW_W: f32 = 160.0;
 const PREVIEW_H: f32 = 72.0;
+const BUTTON_Y: f32 = 252.0;
+const BUTTON_H: f32 = 32.0;
+const BUTTON_W: f32 = 100.0;
+const BUTTON_GAP: f32 = 12.0;
 const WIN_W: f32 = 380.0;
-const WIN_H: f32 = 280.0;
+const WIN_H: f32 = 320.0;
 
 struct RectWidget {
     x: f32, y: f32, w: f32, h: f32,
@@ -85,6 +104,14 @@ struct TextItem {
 #[derive(Clone, Copy, PartialEq)]
 enum DragTarget { Red, Green, Blue }
 
+#[derive(Clone, Copy, PartialEq)]
+enum Action { Apply, Cancel }
+
+struct HitButton {
+    x: f32, y: f32, w: f32, h: f32,
+    action: Action,
+}
+
 struct ColorApp {
     window: Arc<Window>,
     surface: wgpu::Surface<'static>,
@@ -107,10 +134,12 @@ struct ColorApp {
 
     rects: Vec<RectWidget>,
     text_items: Vec<TextItem>,
+    action_buttons: Vec<HitButton>,
 
     cursor_x: f32,
     cursor_y: f32,
     dragging: Option<DragTarget>,
+    action_requested: Option<Action>,
 
     scale_factor: f64,
     width: u32,
@@ -119,7 +148,7 @@ struct ColorApp {
 }
 
 impl ColorApp {
-    async fn new(window: Arc<Window>) -> Self {
+    async fn new(window: Arc<Window>, red: f32, green: f32, blue: f32) -> Self {
         let size = window.inner_size();
         let sw = size.width as f32;
         let sh = size.height as f32;
@@ -207,11 +236,13 @@ impl ColorApp {
         let mut app = Self {
             window, surface, device, queue, config, render_pipeline,
             vertex_buffer, vertex_count: 0,
-            red: 0.5, green: 0.5, blue: 0.5,
+            red, green, blue,
             font_system, swash_cache, text_atlas, text_renderer, text_viewport,
             rects: Vec::new(), text_items: Vec::new(),
+            action_buttons: Vec::new(),
             cursor_x: 0.0, cursor_y: 0.0,
             dragging: None,
+            action_requested: None,
             scale_factor,
             width: size.width, height: size.height,
             needs_rebuild: true,
@@ -220,10 +251,18 @@ impl ColorApp {
         app
     }
 
+    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)
+    }
+
     fn rebuild_layout(&mut self, sw: f32, sh: f32) {
         let s = self.scale_factor as f32;
         let mut rects = Vec::new();
         let mut text_items = Vec::new();
+        let mut action_buttons = Vec::new();
 
         rects.push(RectWidget {
             x: 0.0, y: 0.0, w: sw, h: HEADER_H * s,
@@ -287,10 +326,7 @@ impl ColorApp {
             color: [self.red, self.green, self.blue, 1.0],
         });
 
-        let hex = format!("#{:02X}{:02X}{:02X}",
-            (self.red * 255.0) as u8,
-            (self.green * 255.0) as u8,
-            (self.blue * 255.0) as u8);
+        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,
@@ -298,8 +334,48 @@ impl ColorApp {
             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_bg = [0.20, 0.40, 0.65, 1.0];
+        let cancel_bg = [0.40, 0.20, 0.20, 1.0];
+
+        rects.push(RectWidget {
+            x: apply_x * s, y: btn_y * s,
+            w: BUTTON_W * s, h: BUTTON_H * s,
+            color: btn_bg,
+        });
+        text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, "Apply", 12.0 * s),
+            x: (apply_x + 28.0) * s, y: (btn_y + 8.0) * s,
+            color: glyphon::Color::rgb(0xee, 0xee, 0xf0),
+        });
+        action_buttons.push(HitButton {
+            x: apply_x * s, y: btn_y * s,
+            w: BUTTON_W * s, h: BUTTON_H * s,
+            action: Action::Apply,
+        });
+
+        rects.push(RectWidget {
+            x: cancel_x * s, y: btn_y * s,
+            w: BUTTON_W * s, h: BUTTON_H * s,
+            color: cancel_bg,
+        });
+        text_items.push(TextItem {
+            buffer: make_text_buffer(&mut self.font_system, "Cancel", 12.0 * s),
+            x: (cancel_x + 22.0) * s, y: (btn_y + 8.0) * s,
+            color: glyphon::Color::rgb(0xee, 0xee, 0xf0),
+        });
+        action_buttons.push(HitButton {
+            x: cancel_x * s, y: btn_y * s,
+            w: BUTTON_W * s, h: BUTTON_H * s,
+            action: Action::Cancel,
+        });
+
         self.rects = rects;
         self.text_items = text_items;
+        self.action_buttons = action_buttons;
         self.needs_rebuild = false;
     }
 
@@ -404,6 +480,13 @@ impl ColorApp {
                                 return true;
                             }
                         }
+                        for btn in &self.action_buttons {
+                            if px >= btn.x && px <= btn.x + btn.w && py >= btn.y && py <= btn.y + btn.h {
+                                self.action_requested = Some(btn.action);
+                                self.needs_rebuild = true;
+                                return true;
+                            }
+                        }
                     }
                     ElementState::Released => {
                         if self.dragging.is_some() {
@@ -473,10 +556,17 @@ impl ColorApp {
     }
 }
 
-struct AppWrapper { state: Option<ColorApp> }
+struct AppWrapper {
+    state: Option<ColorApp>,
+    initial_red: f32,
+    initial_green: f32,
+    initial_blue: f32,
+}
 
 impl AppWrapper {
-    fn new() -> Self { Self { state: None } }
+    fn new(red: f32, green: f32, blue: f32) -> Self {
+        Self { state: None, initial_red: red, initial_green: green, initial_blue: blue }
+    }
 }
 
 impl ApplicationHandler for AppWrapper {
@@ -484,10 +574,13 @@ impl ApplicationHandler for AppWrapper {
         if self.state.is_some() { return; }
         let window = Arc::new(event_loop.create_window(
             WindowAttributes::default()
+                .with_name("clear-colors", "clear-colors")
                 .with_title("Clear Colors")
                 .with_inner_size(winit::dpi::LogicalSize::new(WIN_W, WIN_H)),
         ).unwrap());
-        let state = pollster::block_on(ColorApp::new(window));
+        let state = pollster::block_on(ColorApp::new(
+            window, self.initial_red, self.initial_green, self.initial_blue,
+        ));
         self.state = Some(state);
         self.state.as_ref().unwrap().window.request_redraw();
     }
@@ -503,11 +596,27 @@ impl ApplicationHandler for AppWrapper {
             _ => self.state.as_mut().map(|st| st.handle_event(&event)).unwrap_or(false)
         };
         if redraw { if let Some(st) = &mut self.state { st.window.request_redraw(); } }
+        if let Some(st) = &self.state {
+            if let Some(action) = st.action_requested {
+                match action {
+                    Action::Apply => { println!("{}", st.hex()); }
+                    Action::Cancel => {}
+                }
+                event_loop.exit();
+            }
+        }
     }
 }
 
 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))
+    } else {
+        (0.5, 0.5, 0.5)
+    };
+
     let event_loop = EventLoop::new().unwrap();
     event_loop.set_control_flow(ControlFlow::Poll);
-    event_loop.run_app(&mut AppWrapper::new()).unwrap();
+    event_loop.run_app(&mut AppWrapper::new(r, g, b)).unwrap();
 }