gardesk/garlock / db39b04

Browse files

Add text overlay renderer for lock screen

Pango/Cairo-based text rendering for:
- Configurable time display with strftime formats
- Caps Lock warning indicator
- Failed authentication attempts counter
- Cooldown timer display

Includes alpha compositing for overlay blending onto background.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
db39b04f01e7d6dfda4be5c685e61d4f65cb3885
Parents
498655a
Tree
b7f7192

1 changed file

StatusFile+-
A garlock/src/overlay.rs 246 0
garlock/src/overlay.rsadded
@@ -0,0 +1,246 @@
1
+//! Text overlay rendering for garlock
2
+//!
3
+//! Renders text elements like time, caps lock indicator, and failed attempts
4
+//! using Pango for text layout and Cairo for rendering.
5
+
6
+use anyhow::{Context, Result};
7
+use cairo::{Context as CairoContext, Format, ImageSurface};
8
+use chrono::Local;
9
+use pango::FontDescription;
10
+use pangocairo::functions::{create_layout, show_layout};
11
+
12
+use crate::config::{FontConfig, IndicatorConfig};
13
+use crate::ring::Color;
14
+
15
+/// Text overlay renderer
16
+pub struct OverlayRenderer {
17
+    /// Font description for text
18
+    font: FontDescription,
19
+    /// Font description for large text (time)
20
+    font_large: FontDescription,
21
+    /// Text color
22
+    color: Color,
23
+    /// Warning color (for caps lock)
24
+    warning_color: Color,
25
+    /// Error color (for failed attempts)
26
+    error_color: Color,
27
+}
28
+
29
+impl OverlayRenderer {
30
+    /// Create a new overlay renderer from config
31
+    pub fn new(font_config: &FontConfig) -> Self {
32
+        let mut font = FontDescription::new();
33
+        font.set_family(&font_config.family);
34
+        font.set_size(font_config.size as i32 * pango::SCALE);
35
+
36
+        let mut font_large = FontDescription::new();
37
+        font_large.set_family(&font_config.family);
38
+        font_large.set_size((font_config.size * 3) as i32 * pango::SCALE);
39
+
40
+        Self {
41
+            font,
42
+            font_large,
43
+            color: Color::from_hex("#ffffffdd").unwrap_or(Color::white()),
44
+            warning_color: Color::from_hex("#ffaa00dd").unwrap_or(Color::clear()),
45
+            error_color: Color::from_hex("#ff4444dd").unwrap_or(Color::wrong()),
46
+        }
47
+    }
48
+
49
+    /// Render time display
50
+    ///
51
+    /// Returns a surface with the rendered time, or None if time display is disabled.
52
+    pub fn render_time(&self, config: &IndicatorConfig) -> Option<Result<OverlaySurface>> {
53
+        if !config.show_time {
54
+            return None;
55
+        }
56
+
57
+        let time_str = Local::now().format(&config.time_format).to_string();
58
+        Some(self.render_text(&time_str, &self.font_large, self.color))
59
+    }
60
+
61
+    /// Render caps lock indicator
62
+    ///
63
+    /// Returns a surface with the caps lock warning, or None if not applicable.
64
+    pub fn render_caps_lock(
65
+        &self,
66
+        config: &IndicatorConfig,
67
+        caps_active: bool,
68
+    ) -> Option<Result<OverlaySurface>> {
69
+        if !config.show_caps_lock || !caps_active {
70
+            return None;
71
+        }
72
+
73
+        Some(self.render_text(
74
+            &config.caps_lock_text,
75
+            &self.font,
76
+            self.warning_color,
77
+        ))
78
+    }
79
+
80
+    /// Render failed attempts indicator
81
+    ///
82
+    /// Returns a surface showing failed attempt count, or None if no failures.
83
+    pub fn render_failed_attempts(
84
+        &self,
85
+        config: &IndicatorConfig,
86
+        attempts: u32,
87
+    ) -> Option<Result<OverlaySurface>> {
88
+        if !config.show_failed_attempts || attempts == 0 {
89
+            return None;
90
+        }
91
+
92
+        let text = if attempts == 1 {
93
+            "1 failed attempt".to_string()
94
+        } else {
95
+            format!("{} failed attempts", attempts)
96
+        };
97
+
98
+        Some(self.render_text(&text, &self.font, self.error_color))
99
+    }
100
+
101
+    /// Render cooldown timer
102
+    ///
103
+    /// Returns a surface showing remaining cooldown time.
104
+    pub fn render_cooldown(&self, seconds_remaining: u64) -> Option<Result<OverlaySurface>> {
105
+        if seconds_remaining == 0 {
106
+            return None;
107
+        }
108
+
109
+        let text = format!("Try again in {}s", seconds_remaining);
110
+        Some(self.render_text(&text, &self.font, self.error_color))
111
+    }
112
+
113
+    /// Render text to a surface
114
+    fn render_text(
115
+        &self,
116
+        text: &str,
117
+        font: &FontDescription,
118
+        color: Color,
119
+    ) -> Result<OverlaySurface> {
120
+        // Create a temporary surface to measure text
121
+        let temp_surface = ImageSurface::create(Format::ARgb32, 1, 1)
122
+            .context("Failed to create temp surface")?;
123
+        let temp_ctx =
124
+            CairoContext::new(&temp_surface).context("Failed to create temp context")?;
125
+
126
+        let layout = create_layout(&temp_ctx);
127
+        layout.set_font_description(Some(font));
128
+        layout.set_text(text);
129
+
130
+        let (width, height) = layout.pixel_size();
131
+        let padding = 4;
132
+        let surface_width = width + padding * 2;
133
+        let surface_height = height + padding * 2;
134
+
135
+        // Create actual surface
136
+        let surface = ImageSurface::create(Format::ARgb32, surface_width, surface_height)
137
+            .context("Failed to create text surface")?;
138
+        let ctx = CairoContext::new(&surface).context("Failed to create Cairo context")?;
139
+
140
+        // Render text
141
+        ctx.move_to(padding as f64, padding as f64);
142
+        ctx.set_source_rgba(color.r, color.g, color.b, color.a);
143
+
144
+        let layout = create_layout(&ctx);
145
+        layout.set_font_description(Some(font));
146
+        layout.set_text(text);
147
+        show_layout(&ctx, &layout);
148
+
149
+        surface.flush();
150
+
151
+        Ok(OverlaySurface {
152
+            surface,
153
+            width: surface_width,
154
+            height: surface_height,
155
+        })
156
+    }
157
+}
158
+
159
+/// A rendered overlay surface ready for compositing
160
+pub struct OverlaySurface {
161
+    pub surface: ImageSurface,
162
+    pub width: i32,
163
+    pub height: i32,
164
+}
165
+
166
+impl OverlaySurface {
167
+    /// Get the raw pixel data (BGRA format for X11)
168
+    pub fn to_bgra(&mut self) -> Result<Vec<u8>> {
169
+        self.surface.flush();
170
+        let data = self.surface.data().context("Failed to get surface data")?;
171
+        Ok(data.to_vec())
172
+    }
173
+}
174
+
175
+/// Composite an overlay surface onto a background buffer
176
+pub fn composite_overlay(
177
+    background: &mut [u8],
178
+    bg_width: u32,
179
+    bg_height: u32,
180
+    overlay_data: &[u8],
181
+    overlay_width: i32,
182
+    overlay_height: i32,
183
+    dest_x: i32,
184
+    dest_y: i32,
185
+) {
186
+    let bg_stride = bg_width as usize * 4;
187
+    let overlay_stride = overlay_width as usize * 4;
188
+
189
+    for oy in 0..overlay_height {
190
+        let by = dest_y + oy;
191
+        if by < 0 || by >= bg_height as i32 {
192
+            continue;
193
+        }
194
+
195
+        for ox in 0..overlay_width {
196
+            let bx = dest_x + ox;
197
+            if bx < 0 || bx >= bg_width as i32 {
198
+                continue;
199
+            }
200
+
201
+            let overlay_offset = (oy as usize * overlay_stride) + (ox as usize * 4);
202
+            let bg_offset = (by as usize * bg_stride) + (bx as usize * 4);
203
+
204
+            // Overlay pixel (BGRA, premultiplied alpha from Cairo)
205
+            let ob = overlay_data[overlay_offset] as f64 / 255.0;
206
+            let og = overlay_data[overlay_offset + 1] as f64 / 255.0;
207
+            let or = overlay_data[overlay_offset + 2] as f64 / 255.0;
208
+            let oa = overlay_data[overlay_offset + 3] as f64 / 255.0;
209
+
210
+            if oa < 0.001 {
211
+                continue;
212
+            }
213
+
214
+            // Background pixel (BGRA)
215
+            let bb = background[bg_offset] as f64 / 255.0;
216
+            let bg = background[bg_offset + 1] as f64 / 255.0;
217
+            let br = background[bg_offset + 2] as f64 / 255.0;
218
+            let ba = background[bg_offset + 3] as f64 / 255.0;
219
+
220
+            // Alpha compositing
221
+            let out_a = oa + ba * (1.0 - oa);
222
+            if out_a > 0.001 {
223
+                let out_r = (or + br * (1.0 - oa)) / out_a * oa + br * (1.0 - oa);
224
+                let out_g = (og + bg * (1.0 - oa)) / out_a * oa + bg * (1.0 - oa);
225
+                let out_b = (ob + bb * (1.0 - oa)) / out_a * oa + bb * (1.0 - oa);
226
+
227
+                background[bg_offset] = (out_b.min(1.0) * 255.0) as u8;
228
+                background[bg_offset + 1] = (out_g.min(1.0) * 255.0) as u8;
229
+                background[bg_offset + 2] = (out_r.min(1.0) * 255.0) as u8;
230
+                background[bg_offset + 3] = (out_a.min(1.0) * 255.0) as u8;
231
+            }
232
+        }
233
+    }
234
+}
235
+
236
+// Extend Color with additional helpers
237
+impl Color {
238
+    pub fn white() -> Self {
239
+        Self {
240
+            r: 1.0,
241
+            g: 1.0,
242
+            b: 1.0,
243
+            a: 0.87,
244
+        }
245
+    }
246
+}