gardesk/garlock / f3eb3e6

Browse files

Add swaylock-style ring indicator

- Cairo-based circular ring rendering
- State-based colors: idle, typing, verifying, wrong, clear
- Segment highlighting for keystroke feedback
- Alpha compositing onto background
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
f3eb3e6e130403366e32be77f3f56b41cdba3bcc
Parents
c6c3d0f
Tree
677cb83

2 changed files

StatusFile+-
A garlock/src/ring/mod.rs 122 0
A garlock/src/ring/renderer.rs 280 0
garlock/src/ring/mod.rsadded
@@ -0,0 +1,122 @@
1
+//! Ring indicator module for garlock
2
+//!
3
+//! Renders a swaylock-style circular ring that provides visual feedback
4
+//! for the current lock state.
5
+
6
+pub mod renderer;
7
+
8
+pub use renderer::{composite_ring, RingRenderer};
9
+
10
+/// Ring indicator state
11
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12
+pub enum RingState {
13
+    /// Waiting for input (blue)
14
+    #[default]
15
+    Idle,
16
+    /// Receiving password input (green)
17
+    Typing,
18
+    /// Verifying password with PAM (orange)
19
+    Verifying,
20
+    /// Authentication failed (red)
21
+    Wrong,
22
+    /// Backspace/clearing input (yellow)
23
+    Clear,
24
+}
25
+
26
+impl RingState {
27
+    /// Get the display name for this state
28
+    pub fn name(&self) -> &'static str {
29
+        match self {
30
+            RingState::Idle => "idle",
31
+            RingState::Typing => "typing",
32
+            RingState::Verifying => "verifying",
33
+            RingState::Wrong => "wrong",
34
+            RingState::Clear => "clear",
35
+        }
36
+    }
37
+}
38
+
39
+/// RGBA color with components in 0.0-1.0 range
40
+#[derive(Debug, Clone, Copy)]
41
+pub struct Color {
42
+    pub r: f64,
43
+    pub g: f64,
44
+    pub b: f64,
45
+    pub a: f64,
46
+}
47
+
48
+impl Color {
49
+    /// Create a new color from RGBA components (0.0-1.0)
50
+    pub fn new(r: f64, g: f64, b: f64, a: f64) -> Self {
51
+        Self { r, g, b, a }
52
+    }
53
+
54
+    /// Parse a hex color string like "#1e90ffcc" or "#1e90ff"
55
+    pub fn from_hex(hex: &str) -> Option<Self> {
56
+        let hex = hex.trim_start_matches('#');
57
+
58
+        match hex.len() {
59
+            6 => {
60
+                // RGB without alpha
61
+                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
62
+                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
63
+                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
64
+                Some(Self {
65
+                    r: r as f64 / 255.0,
66
+                    g: g as f64 / 255.0,
67
+                    b: b as f64 / 255.0,
68
+                    a: 1.0,
69
+                })
70
+            }
71
+            8 => {
72
+                // RGBA with alpha
73
+                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
74
+                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
75
+                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
76
+                let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
77
+                Some(Self {
78
+                    r: r as f64 / 255.0,
79
+                    g: g as f64 / 255.0,
80
+                    b: b as f64 / 255.0,
81
+                    a: a as f64 / 255.0,
82
+                })
83
+            }
84
+            _ => None,
85
+        }
86
+    }
87
+
88
+    /// Default blue for idle state
89
+    pub fn idle() -> Self {
90
+        Self::from_hex("#1e90ffcc").unwrap()
91
+    }
92
+
93
+    /// Default green for typing state
94
+    pub fn typing() -> Self {
95
+        Self::from_hex("#00ff00cc").unwrap()
96
+    }
97
+
98
+    /// Default orange for verifying state
99
+    pub fn verifying() -> Self {
100
+        Self::from_hex("#ffa500cc").unwrap()
101
+    }
102
+
103
+    /// Default red for wrong state
104
+    pub fn wrong() -> Self {
105
+        Self::from_hex("#ff0000cc").unwrap()
106
+    }
107
+
108
+    /// Default yellow for clear state
109
+    pub fn clear() -> Self {
110
+        Self::from_hex("#ffff00cc").unwrap()
111
+    }
112
+
113
+    /// Default dark color for inner circle
114
+    pub fn inside() -> Self {
115
+        Self::from_hex("#00000088").unwrap()
116
+    }
117
+
118
+    /// Default color for ring background track
119
+    pub fn ring_bg() -> Self {
120
+        Self::from_hex("#00000055").unwrap()
121
+    }
122
+}
garlock/src/ring/renderer.rsadded
@@ -0,0 +1,280 @@
1
+//! Ring indicator renderer using Cairo
2
+//!
3
+//! Renders the circular ring indicator with state-based colors and
4
+//! segment highlighting for keystroke feedback.
5
+
6
+use std::f64::consts::PI;
7
+
8
+use anyhow::{Context, Result};
9
+use cairo::{Context as CairoContext, Format, ImageSurface, Operator};
10
+
11
+use super::{Color, RingState};
12
+use crate::config::RingConfig;
13
+
14
+/// Number of segments around the ring for keystroke feedback
15
+const NUM_SEGMENTS: usize = 12;
16
+
17
+/// Ring indicator renderer
18
+pub struct RingRenderer {
19
+    /// Outer radius of the ring
20
+    outer_radius: f64,
21
+    /// Inner radius of the ring (dark center)
22
+    inner_radius: f64,
23
+    /// Line width for ring stroke
24
+    line_width: f64,
25
+    /// Colors for each state
26
+    color_idle: Color,
27
+    color_typing: Color,
28
+    color_verifying: Color,
29
+    color_wrong: Color,
30
+    color_clear: Color,
31
+    color_inside: Color,
32
+    color_ring_bg: Color,
33
+    /// Current state
34
+    state: RingState,
35
+    /// Current highlighted segment (0-11, or None)
36
+    highlight_segment: Option<usize>,
37
+}
38
+
39
+impl RingRenderer {
40
+    /// Create a new ring renderer from config
41
+    pub fn from_config(config: &RingConfig) -> Self {
42
+        Self {
43
+            outer_radius: config.radius_outer,
44
+            inner_radius: config.radius_inner,
45
+            line_width: config.line_width,
46
+            color_idle: Color::from_hex(&config.color_idle).unwrap_or_else(Color::idle),
47
+            color_typing: Color::from_hex(&config.color_typing).unwrap_or_else(Color::typing),
48
+            color_verifying: Color::from_hex(&config.color_verifying)
49
+                .unwrap_or_else(Color::verifying),
50
+            color_wrong: Color::from_hex(&config.color_wrong).unwrap_or_else(Color::wrong),
51
+            color_clear: Color::from_hex(&config.color_clear).unwrap_or_else(Color::clear),
52
+            color_inside: Color::from_hex(&config.color_inside).unwrap_or_else(Color::inside),
53
+            color_ring_bg: Color::from_hex(&config.color_ring_bg).unwrap_or_else(Color::ring_bg),
54
+            state: RingState::Idle,
55
+            highlight_segment: None,
56
+        }
57
+    }
58
+
59
+    /// Set the current ring state
60
+    pub fn set_state(&mut self, state: RingState) {
61
+        self.state = state;
62
+    }
63
+
64
+    /// Get the current ring state
65
+    pub fn state(&self) -> RingState {
66
+        self.state
67
+    }
68
+
69
+    /// Set the highlighted segment (for keystroke feedback)
70
+    pub fn set_highlight_segment(&mut self, segment: Option<usize>) {
71
+        self.highlight_segment = segment.map(|s| s % NUM_SEGMENTS);
72
+    }
73
+
74
+    /// Advance highlight to next segment
75
+    pub fn advance_highlight(&mut self) {
76
+        self.highlight_segment = Some(
77
+            self.highlight_segment
78
+                .map(|s| (s + 1) % NUM_SEGMENTS)
79
+                .unwrap_or(0),
80
+        );
81
+    }
82
+
83
+    /// Retreat highlight to previous segment (for backspace)
84
+    ///
85
+    /// If at segment 0, clears the highlight entirely.
86
+    pub fn retreat_highlight(&mut self) {
87
+        self.highlight_segment = self.highlight_segment.and_then(|s| {
88
+            if s == 0 {
89
+                None // At start, clear highlight
90
+            } else {
91
+                Some(s - 1)
92
+            }
93
+        });
94
+    }
95
+
96
+    /// Clear the highlight
97
+    pub fn clear_highlight(&mut self) {
98
+        self.highlight_segment = None;
99
+    }
100
+
101
+    /// Get the color for the current state
102
+    fn state_color(&self) -> Color {
103
+        match self.state {
104
+            RingState::Idle => self.color_idle,
105
+            RingState::Typing => self.color_typing,
106
+            RingState::Verifying => self.color_verifying,
107
+            RingState::Wrong => self.color_wrong,
108
+            RingState::Clear => self.color_clear,
109
+        }
110
+    }
111
+
112
+    /// Render the ring to a new Cairo surface
113
+    ///
114
+    /// Returns a surface sized to fit the ring with some padding.
115
+    pub fn render(&self) -> Result<ImageSurface> {
116
+        let padding = 10.0;
117
+        let size = (self.outer_radius * 2.0 + padding * 2.0).ceil() as i32;
118
+
119
+        let surface = ImageSurface::create(Format::ARgb32, size, size)
120
+            .context("Failed to create ring surface")?;
121
+
122
+        let ctx = CairoContext::new(&surface).context("Failed to create Cairo context")?;
123
+
124
+        // Center of the surface
125
+        let cx = size as f64 / 2.0;
126
+        let cy = size as f64 / 2.0;
127
+
128
+        self.draw(&ctx, cx, cy)?;
129
+
130
+        surface.flush();
131
+        Ok(surface)
132
+    }
133
+
134
+    /// Draw the ring at the specified center coordinates
135
+    pub fn draw(&self, ctx: &CairoContext, cx: f64, cy: f64) -> Result<()> {
136
+        // Draw inner dark circle (background)
137
+        ctx.arc(cx, cy, self.inner_radius, 0.0, 2.0 * PI);
138
+        ctx.set_source_rgba(
139
+            self.color_inside.r,
140
+            self.color_inside.g,
141
+            self.color_inside.b,
142
+            self.color_inside.a,
143
+        );
144
+        ctx.fill()?;
145
+
146
+        // Draw ring background track
147
+        let ring_center_radius = (self.outer_radius + self.inner_radius) / 2.0;
148
+        ctx.set_line_width(self.line_width);
149
+        ctx.arc(cx, cy, ring_center_radius, 0.0, 2.0 * PI);
150
+        ctx.set_source_rgba(
151
+            self.color_ring_bg.r,
152
+            self.color_ring_bg.g,
153
+            self.color_ring_bg.b,
154
+            self.color_ring_bg.a,
155
+        );
156
+        ctx.stroke()?;
157
+
158
+        // Draw ring with state color
159
+        let color = self.state_color();
160
+        ctx.set_line_width(self.line_width);
161
+        ctx.arc(cx, cy, ring_center_radius, 0.0, 2.0 * PI);
162
+        ctx.set_source_rgba(color.r, color.g, color.b, color.a);
163
+        ctx.stroke()?;
164
+
165
+        // Draw segment highlight if active
166
+        if let Some(segment) = self.highlight_segment {
167
+            self.draw_segment_highlight(ctx, cx, cy, segment)?;
168
+        }
169
+
170
+        Ok(())
171
+    }
172
+
173
+    /// Draw a highlighted segment
174
+    fn draw_segment_highlight(
175
+        &self,
176
+        ctx: &CairoContext,
177
+        cx: f64,
178
+        cy: f64,
179
+        segment: usize,
180
+    ) -> Result<()> {
181
+        let segment_angle = 2.0 * PI / NUM_SEGMENTS as f64;
182
+        // Start from top (-PI/2) and go clockwise
183
+        let start_angle = -PI / 2.0 + (segment as f64 * segment_angle);
184
+        let end_angle = start_angle + segment_angle;
185
+
186
+        let ring_center_radius = (self.outer_radius + self.inner_radius) / 2.0;
187
+
188
+        ctx.set_line_width(self.line_width + 2.0);
189
+        ctx.arc(cx, cy, ring_center_radius, start_angle, end_angle);
190
+        // Brighter version of current state color
191
+        let color = self.state_color();
192
+        ctx.set_source_rgba(
193
+            (color.r + 0.3).min(1.0),
194
+            (color.g + 0.3).min(1.0),
195
+            (color.b + 0.3).min(1.0),
196
+            color.a,
197
+        );
198
+        ctx.stroke()?;
199
+
200
+        Ok(())
201
+    }
202
+
203
+    /// Get the size needed for the ring (width and height)
204
+    pub fn size(&self) -> (i32, i32) {
205
+        let padding = 10.0;
206
+        let size = (self.outer_radius * 2.0 + padding * 2.0).ceil() as i32;
207
+        (size, size)
208
+    }
209
+
210
+    /// Get the raw pixel data from a surface (BGRA format for X11)
211
+    pub fn surface_to_bgra(surface: &mut ImageSurface) -> Result<Vec<u8>> {
212
+        surface.flush();
213
+        let data = surface.data().context("Failed to get surface data")?;
214
+        Ok(data.to_vec())
215
+    }
216
+}
217
+
218
+/// Composite the ring onto a background buffer at the specified position
219
+///
220
+/// Both buffers are in BGRA format. The ring is alpha-blended onto the background.
221
+pub fn composite_ring(
222
+    background: &mut [u8],
223
+    bg_width: u32,
224
+    bg_height: u32,
225
+    ring_data: &[u8],
226
+    ring_width: u32,
227
+    ring_height: u32,
228
+    dest_x: i32,
229
+    dest_y: i32,
230
+) {
231
+    let bg_stride = bg_width as usize * 4;
232
+    let ring_stride = ring_width as usize * 4;
233
+
234
+    for ry in 0..ring_height as i32 {
235
+        let by = dest_y + ry;
236
+        if by < 0 || by >= bg_height as i32 {
237
+            continue;
238
+        }
239
+
240
+        for rx in 0..ring_width as i32 {
241
+            let bx = dest_x + rx;
242
+            if bx < 0 || bx >= bg_width as i32 {
243
+                continue;
244
+            }
245
+
246
+            let ring_offset = (ry as usize * ring_stride) + (rx as usize * 4);
247
+            let bg_offset = (by as usize * bg_stride) + (bx as usize * 4);
248
+
249
+            // Ring pixel (BGRA)
250
+            let rb = ring_data[ring_offset] as f64 / 255.0;
251
+            let rg = ring_data[ring_offset + 1] as f64 / 255.0;
252
+            let rr = ring_data[ring_offset + 2] as f64 / 255.0;
253
+            let ra = ring_data[ring_offset + 3] as f64 / 255.0;
254
+
255
+            if ra < 0.001 {
256
+                // Fully transparent, skip
257
+                continue;
258
+            }
259
+
260
+            // Background pixel (BGRA)
261
+            let bb = background[bg_offset] as f64 / 255.0;
262
+            let bg = background[bg_offset + 1] as f64 / 255.0;
263
+            let br = background[bg_offset + 2] as f64 / 255.0;
264
+            let ba = background[bg_offset + 3] as f64 / 255.0;
265
+
266
+            // Alpha compositing (Porter-Duff "over" operator)
267
+            let out_a = ra + ba * (1.0 - ra);
268
+            if out_a > 0.001 {
269
+                let out_r = (rr * ra + br * ba * (1.0 - ra)) / out_a;
270
+                let out_g = (rg * ra + bg * ba * (1.0 - ra)) / out_a;
271
+                let out_b = (rb * ra + bb * ba * (1.0 - ra)) / out_a;
272
+
273
+                background[bg_offset] = (out_b * 255.0) as u8;
274
+                background[bg_offset + 1] = (out_g * 255.0) as u8;
275
+                background[bg_offset + 2] = (out_r * 255.0) as u8;
276
+                background[bg_offset + 3] = (out_a * 255.0) as u8;
277
+            }
278
+        }
279
+    }
280
+}