Rust · 7894 bytes Raw Blame History
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 }
247