gardesk/gardisplay / 1ff5261

Browse files

add Apply/Revert/Save buttons with profile save/load

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
1ff5261ad9e30e8e4f2a1033d5397c26847fe5e7
Parents
0626bbb
Tree
4dadded

3 changed files

StatusFile+-
M gardisplay/src/app.rs 141 16
M gardisplay/src/ui/mod.rs 2 0
M gardisplay/src/ui/monitor_view.rs 15 0
gardisplay/src/app.rsmodified
@@ -9,9 +9,9 @@ use gartk_x11::{
99
 };
1010
 use x11rb::protocol::xproto::ConnectionExt;
1111
 
12
-use crate::config::{Config, MonitorConfig};
12
+use crate::config::{Config, MonitorConfig, Profile};
1313
 use crate::randr::RandrManager;
14
-use crate::ui::{EventResult, MonitorView};
14
+use crate::ui::{Button, EventResult, MonitorView};
1515
 
1616
 /// Window dimensions.
1717
 const WINDOW_WIDTH: u32 = 800;
@@ -25,13 +25,16 @@ pub struct App {
2525
     renderer: Renderer,
2626
     theme: Theme,
2727
     gc: u32,
28
-    #[allow(dead_code)] // Used for profile management
2928
     config: Config,
3029
     monitor_view: MonitorView,
3130
     randr: Option<RandrManager>,
3231
     original_monitors: Vec<Monitor>,
3332
     demo_mode: bool,
3433
     status_message: Option<(String, std::time::Instant)>,
34
+    // UI buttons
35
+    btn_apply: Button,
36
+    btn_revert: Button,
37
+    btn_save: Button,
3538
 }
3639
 
3740
 impl App {
@@ -125,6 +128,18 @@ impl App {
125128
         let original_monitors = monitors.clone();
126129
         monitor_view.set_monitors(monitors);
127130
 
131
+        // Try to load saved profile
132
+        if let Some(profile) = config.profiles.get(&config.general.default_profile) {
133
+            tracing::info!("loading profile '{}'", config.general.default_profile);
134
+            Self::apply_profile_to_view(&mut monitor_view, profile);
135
+        }
136
+
137
+        // Create UI buttons (positioned in controls area)
138
+        let controls_y = (WINDOW_HEIGHT - 100) as i32;
139
+        let btn_apply = Button::new(WINDOW_WIDTH as i32 - 280, controls_y + 30, 80, 32, "Apply");
140
+        let btn_revert = Button::new(WINDOW_WIDTH as i32 - 190, controls_y + 30, 80, 32, "Revert");
141
+        let btn_save = Button::new(WINDOW_WIDTH as i32 - 100, controls_y + 30, 80, 32, "Save");
142
+
128143
         Ok(Self {
129144
             conn,
130145
             window,
@@ -137,9 +152,43 @@ impl App {
137152
             original_monitors,
138153
             demo_mode: demo,
139154
             status_message: None,
155
+            btn_apply,
156
+            btn_revert,
157
+            btn_save,
140158
         })
141159
     }
142160
 
161
+    /// Apply a saved profile to the monitor view.
162
+    fn apply_profile_to_view(view: &mut MonitorView, profile: &Profile) {
163
+        // Build a map of monitor name -> config
164
+        let config_map: std::collections::HashMap<&str, &MonitorConfig> = profile
165
+            .monitors
166
+            .iter()
167
+            .map(|m| (m.name.as_str(), m))
168
+            .collect();
169
+
170
+        // Update monitor positions from profile
171
+        for state in view.monitors_mut() {
172
+            if let Some(config) = config_map.get(state.info.name.as_str()) {
173
+                state.real_position = gartk_core::Point::new(config.x, config.y);
174
+                tracing::debug!(
175
+                    "loaded {} at ({}, {})",
176
+                    state.info.name,
177
+                    config.x,
178
+                    config.y
179
+                );
180
+            }
181
+        }
182
+
183
+        // Set primary monitor
184
+        if let Some(ref primary) = profile.primary {
185
+            view.set_primary(primary);
186
+        }
187
+
188
+        // Recalculate scaling
189
+        view.recalculate_layout();
190
+    }
191
+
143192
     /// Create demo monitors for UI testing.
144193
     fn demo_monitors() -> Vec<gartk_x11::Monitor> {
145194
         vec![
@@ -200,6 +249,26 @@ impl App {
200249
 
201250
     /// Handle an input event.
202251
     fn handle_event(&mut self, event: &InputEvent) -> EventResult {
252
+        // Handle button clicks
253
+        if self.btn_apply.handle_event(event) {
254
+            self.apply_layout();
255
+            return EventResult::Redraw;
256
+        }
257
+        if self.btn_revert.handle_event(event) {
258
+            self.revert_layout();
259
+            return EventResult::Redraw;
260
+        }
261
+        if self.btn_save.handle_event(event) {
262
+            self.save_profile();
263
+            return EventResult::Redraw;
264
+        }
265
+
266
+        // Check if any button needs redraw from hover
267
+        if matches!(event, InputEvent::MouseMove(_)) {
268
+            // Buttons handle their own hover state
269
+            return EventResult::Redraw;
270
+        }
271
+
203272
         match event {
204273
             InputEvent::CloseRequested => EventResult::Quit,
205274
             InputEvent::Key(e) if e.pressed && e.key == Key::Escape => EventResult::Quit,
@@ -222,6 +291,11 @@ impl App {
222291
                 self.revert_layout();
223292
                 EventResult::Redraw
224293
             }
294
+            // Save: Ctrl+S
295
+            InputEvent::Key(e) if e.pressed && e.modifiers.ctrl && e.key == Key::Char('s') => {
296
+                self.save_profile();
297
+                EventResult::Redraw
298
+            }
225299
             InputEvent::Resize { width, height } => {
226300
                 self.handle_resize(Size::new(*width, *height));
227301
                 EventResult::Redraw
@@ -365,6 +439,47 @@ impl App {
365439
         self.status_message = Some((message.to_string(), std::time::Instant::now()));
366440
     }
367441
 
442
+    /// Save current layout as a profile.
443
+    fn save_profile(&mut self) {
444
+        let primary_name = self.monitor_view.primary_name().map(|s| s.to_string());
445
+
446
+        // Build profile from current layout
447
+        let monitors: Vec<MonitorConfig> = self
448
+            .monitor_view
449
+            .monitors()
450
+            .iter()
451
+            .map(|state| MonitorConfig {
452
+                name: state.info.name.clone(),
453
+                enabled: true,
454
+                x: state.real_position.x,
455
+                y: state.real_position.y,
456
+                width: state.info.rect.width,
457
+                height: state.info.rect.height,
458
+                refresh: 60.0,
459
+                scale: 1.0,
460
+                rotation: 0,
461
+            })
462
+            .collect();
463
+
464
+        let profile = Profile {
465
+            primary: primary_name,
466
+            monitors,
467
+        };
468
+
469
+        // Save to default profile
470
+        self.config
471
+            .profiles
472
+            .insert("default".to_string(), profile);
473
+
474
+        match self.config.save() {
475
+            Ok(()) => self.set_status("Saved profile"),
476
+            Err(e) => {
477
+                tracing::error!("failed to save profile: {}", e);
478
+                self.set_status("Failed to save profile");
479
+            }
480
+        }
481
+    }
482
+
368483
     /// Handle window resize.
369484
     fn handle_resize(&mut self, size: Size) {
370485
         tracing::debug!("resize to {}x{}", size.width, size.height);
@@ -378,6 +493,12 @@ impl App {
378493
         // Update monitor view rect
379494
         let view_rect = Rect::new(0, 0, size.width, size.height.saturating_sub(100));
380495
         self.monitor_view.set_view_rect(view_rect);
496
+
497
+        // Reposition buttons
498
+        let controls_y = size.height.saturating_sub(100) as i32;
499
+        self.btn_apply.set_position(size.width as i32 - 280, controls_y + 30);
500
+        self.btn_revert.set_position(size.width as i32 - 190, controls_y + 30);
501
+        self.btn_save.set_position(size.width as i32 - 100, controls_y + 30);
381502
     }
382503
 
383504
     /// Render the application.
@@ -405,46 +526,50 @@ impl App {
405526
             1.0,
406527
         )?;
407528
 
408
-        // Instructions
529
+        // Left side: Instructions and status
409530
         self.renderer.text_default(
410
-            "Drag monitors to arrange | Double-click to set primary",
531
+            "Drag monitors to arrange | Double-click: set primary",
411532
             10.0,
412
-            (controls_y + 20) as f64,
533
+            (controls_y + 15) as f64,
413534
             self.theme.foreground,
414535
         )?;
415536
 
416
-        // Keyboard shortcuts
537
+        // Keyboard shortcuts hint
417538
         self.renderer.text_default(
418
-            "Enter: Apply | Backspace: Revert | Q/Esc: Quit",
539
+            "Ctrl+S: Save | Ctrl+A: Apply | Ctrl+R: Revert",
419540
             10.0,
420
-            (controls_y + 40) as f64,
541
+            (controls_y + 35) as f64,
421542
             self.theme.item_description,
422543
         )?;
423544
 
424545
         // Status message (show for 3 seconds)
425546
         if let Some((ref msg, instant)) = self.status_message {
426547
             if instant.elapsed().as_secs() < 3 {
427
-                // Green for success, theme color otherwise
428
-                let color = if msg.contains("error") {
548
+                let color = if msg.contains("error") || msg.contains("Failed") {
429549
                     Color::new(1.0, 0.4, 0.4, 1.0) // Red
430550
                 } else {
431551
                     Color::new(0.4, 0.8, 0.4, 1.0) // Green
432552
                 };
433553
                 self.renderer
434
-                    .text_default(msg, 10.0, (controls_y + 70) as f64, color)?;
554
+                    .text_default(msg, 10.0, (controls_y + 60) as f64, color)?;
435555
             }
436556
         }
437557
 
438
-        // Dirty indicator
558
+        // Dirty indicator (above buttons)
439559
         if self.monitor_view.is_dirty() {
440560
             self.renderer.text_default(
441
-                "(unsaved changes)",
442
-                (size.width - 150) as f64,
443
-                (controls_y + 20) as f64,
561
+                "* unsaved",
562
+                (size.width - 280) as f64,
563
+                (controls_y + 15) as f64,
444564
                 Color::new(1.0, 0.7, 0.3, 1.0), // Orange
445565
             )?;
446566
         }
447567
 
568
+        // Render buttons
569
+        self.btn_apply.render(&self.renderer, &self.theme)?;
570
+        self.btn_revert.render(&self.renderer, &self.theme)?;
571
+        self.btn_save.render(&self.renderer, &self.theme)?;
572
+
448573
         // Blit to window
449574
         copy_surface_to_window(self.renderer.surface_mut(), &self.window, self.gc, 0, 0)?;
450575
 
gardisplay/src/ui/mod.rsmodified
@@ -1,8 +1,10 @@
11
 //! UI components for gardisplay.
22
 
33
 mod monitor_view;
4
+pub mod widgets;
45
 
56
 pub use monitor_view::MonitorView;
7
+pub use widgets::Button;
68
 
79
 /// Result of handling an event.
810
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
gardisplay/src/ui/monitor_view.rsmodified
@@ -629,6 +629,21 @@ impl MonitorView {
629629
         &self.monitors
630630
     }
631631
 
632
+    /// Get mutable monitors.
633
+    pub fn monitors_mut(&mut self) -> &mut [MonitorState] {
634
+        &mut self.monitors
635
+    }
636
+
637
+    /// Set primary monitor by name.
638
+    pub fn set_primary(&mut self, name: &str) {
639
+        self.primary_name = Some(name.to_string());
640
+    }
641
+
642
+    /// Recalculate layout from real positions.
643
+    pub fn recalculate_layout(&mut self) {
644
+        self.recalculate_scaled_rects();
645
+    }
646
+
632647
     /// Check if layout has been modified.
633648
     #[allow(dead_code)] // Used in Sprint 3 for RandR application
634649
     pub fn is_dirty(&self) -> bool {