1use crate::font;
21
22pub const PERMISSIONS_PROMPT_HEIGHT: u32 = 60;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct PermissionsPrompt {
31 pub origin: String,
35 pub capabilities: Vec<String>,
38 pub queue_len: u32,
41}
42
43impl PermissionsPrompt {
44 pub fn paint_at(
49 &self,
50 buffer: &mut [u32],
51 width: usize,
52 height: usize,
53 content_x: u32,
54 content_y: u32,
55 content_w: u32,
56 ) -> u32 {
57 if width == 0 || height == 0 || content_w == 0 {
58 return PERMISSIONS_PROMPT_HEIGHT;
59 }
60 if buffer.len() < width * height {
61 return PERMISSIONS_PROMPT_HEIGHT;
62 }
63 let top = content_y as i32;
64 if top >= height as i32 {
65 return PERMISSIONS_PROMPT_HEIGHT;
66 }
67
68 let text_x: i32 = content_x as i32 + 8;
70 let text_y0 = top + 8;
71 let text_y1 = top + 8 + (font::glyph_h() as i32 + 8);
72
73 let caps_joined = self.capabilities.join(", ");
74 let line1 = if caps_joined.is_empty() {
75 format!("{} wants permission", self.origin)
76 } else {
77 format!("{} wants: {caps_joined}", self.origin)
78 };
79
80 let queue_text = if self.queue_len > 0 {
81 Some(format!("({} more pending)", self.queue_len))
82 } else {
83 None
84 };
85 let queue_w = queue_text
86 .as_ref()
87 .map(|s| font::text_width(s) as i32)
88 .unwrap_or(0);
89 let right_pad: i32 = 8;
90 let right_edge = (content_x + content_w) as i32;
91 let line1_max_px = if queue_text.is_some() {
92 (right_edge - text_x - queue_w - right_pad - 12).max(0) as usize
93 } else {
94 (right_edge - text_x - right_pad).max(0) as usize
95 };
96 let line1_truncated = truncate_to_width(&line1, line1_max_px);
97 font::draw_text(
98 buffer,
99 width,
100 height,
101 text_x,
102 text_y0,
103 line1_truncated,
104 COLOUR_PROMPT_FG,
105 );
106
107 if let Some(qtext) = queue_text {
108 let qx = right_edge - right_pad - queue_w;
109 font::draw_text(
110 buffer,
111 width,
112 height,
113 qx,
114 text_y0,
115 &qtext,
116 COLOUR_PROMPT_PENDING,
117 );
118 }
119
120 font::draw_text(
121 buffer,
122 width,
123 height,
124 text_x,
125 text_y1,
126 ACTION_HINT,
127 COLOUR_PROMPT_HINT,
128 );
129
130 PERMISSIONS_PROMPT_HEIGHT
131 }
132}
133
134pub const ACTION_HINT: &str = "[a]llow [d]eny [A]llow always [D]eny always [Esc]defer";
137
138fn truncate_to_width(s: &str, max_px: usize) -> &str {
142 if font::text_width(s) <= max_px {
143 return s;
144 }
145 if max_px < font::text_width("..") {
146 return "";
147 }
148 let mut end = s.len();
149 while end > 0 {
150 let prefix = &s[..end];
151 if !s.is_char_boundary(end) {
152 end -= 1;
153 continue;
154 }
155 if font::text_width(prefix) + font::text_width("..") <= max_px {
156 return prefix;
157 }
158 end -= 1;
159 }
160 ""
161}
162
163const COLOUR_PROMPT_FG: u32 = 0xFF_F0_E8_D8;
164const COLOUR_PROMPT_HINT: u32 = 0xFF_C8_C8_C0;
165const COLOUR_PROMPT_PENDING: u32 = 0xFF_FF_C8_8C;
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 fn make_buf(w: usize, h: usize) -> Vec<u32> {
172 vec![0u32; w * h]
173 }
174
175 #[test]
176 fn paint_at_does_not_panic_normal_case() {
177 let w = 800;
178 let h = 400;
179 let mut buf = make_buf(w, h);
180 let p = PermissionsPrompt {
181 origin: "https://example.com".into(),
182 capabilities: vec!["camera".into()],
183 queue_len: 0,
184 };
185 let ret = p.paint_at(&mut buf, w, h, 100, 100, 600);
186 assert_eq!(ret, PERMISSIONS_PROMPT_HEIGHT);
187 assert!(buf.iter().any(|&px| px != 0));
189 }
190
191 #[test]
192 fn paint_at_queue_and_no_queue_differ() {
193 let w = 800;
194 let h = 400;
195 let mut buf_zero = make_buf(w, h);
196 let mut buf_nonzero = make_buf(w, h);
197 let zero = PermissionsPrompt {
198 origin: "https://x".into(),
199 capabilities: vec!["camera".into()],
200 queue_len: 0,
201 };
202 let nonzero = PermissionsPrompt {
203 origin: "https://x".into(),
204 capabilities: vec!["camera".into()],
205 queue_len: 3,
206 };
207 zero.paint_at(&mut buf_zero, w, h, 100, 100, 600);
208 nonzero.paint_at(&mut buf_nonzero, w, h, 100, 100, 600);
209 assert_ne!(buf_zero, buf_nonzero);
210 }
211
212 #[test]
213 fn paint_at_skips_when_top_y_off_screen() {
214 let w = 200;
215 let h = 60;
216 let mut buf = make_buf(w, h);
217 let p = PermissionsPrompt {
218 origin: "x".into(),
219 capabilities: vec![],
220 queue_len: 0,
221 };
222 p.paint_at(&mut buf, w, h, 0, 1000, 200);
223 assert!(buf.iter().all(|&px| px == 0));
224 }
225
226 #[test]
227 fn paint_at_handles_empty_capabilities_list() {
228 let w = 600;
229 let h = 400;
230 let mut buf = make_buf(w, h);
231 let p = PermissionsPrompt {
232 origin: "https://x".into(),
233 capabilities: vec![],
234 queue_len: 0,
235 };
236 let ret = p.paint_at(&mut buf, w, h, 0, 0, 600);
237 assert_eq!(ret, PERMISSIONS_PROMPT_HEIGHT);
238 }
239
240 #[test]
241 fn truncate_to_width_short_unchanged() {
242 assert_eq!(truncate_to_width("ok", 1000), "ok");
243 }
244
245 #[test]
246 fn truncate_to_width_zero_budget_returns_empty() {
247 assert_eq!(truncate_to_width("anything", 1), "");
248 }
249}