1use std::time::{Duration, Instant};
4
5use crate::{
6 backend::{MouseButton, Window, WindowEvent, create_window},
7 error::Error,
8 render::{Canvas, Font, rgb},
9 ui::{
10 BASE_BUTTON_HEIGHT, BASE_BUTTON_SPACING, BASE_CORNER_RADIUS, ButtonPreset, Colors,
11 DialogResult, Icon, KEY_ESCAPE, KEY_RETURN,
12 widgets::{Widget, button::Button},
13 },
14};
15
16const BASE_ICON_SIZE: u32 = 48;
17const BASE_PADDING: u32 = 20;
18const BASE_MIN_WIDTH: u32 = 150;
19const BASE_MAX_TEXT_WIDTH: f32 = 150.0;
20
21pub struct MessageBuilder {
23 title: String,
24 text: String,
25 icon: Option<Icon>,
26 buttons: ButtonPreset,
27 timeout: Option<u32>,
28 width: Option<u32>,
29 height: Option<u32>,
30 no_wrap: bool,
31 no_markup: bool,
32 ellipsize: bool,
33 switch: bool,
34 extra_buttons: Vec<String>,
35 colors: Option<&'static Colors>,
36}
37
38impl MessageBuilder {
39 pub fn new() -> Self {
40 Self {
41 title: String::new(),
42 text: String::new(),
43 icon: None,
44 buttons: ButtonPreset::Ok,
45 timeout: None,
46 width: None,
47 height: None,
48 no_wrap: false,
49 no_markup: false,
50 ellipsize: false,
51 switch: false,
52 extra_buttons: Vec::new(),
53 colors: None,
54 }
55 }
56
57 pub fn timeout(mut self, seconds: u32) -> Self {
59 self.timeout = Some(seconds);
60 self
61 }
62
63 pub fn title(mut self, title: &str) -> Self {
64 self.title = title.to_string();
65 self
66 }
67
68 pub fn text(mut self, text: &str) -> Self {
69 self.text = text.to_string();
70 self
71 }
72
73 pub fn icon(mut self, icon: Icon) -> Self {
74 self.icon = Some(icon);
75 self
76 }
77
78 pub fn buttons(mut self, buttons: ButtonPreset) -> Self {
79 self.buttons = buttons;
80 self
81 }
82
83 pub fn colors(mut self, colors: &'static Colors) -> Self {
84 self.colors = Some(colors);
85 self
86 }
87
88 pub fn width(mut self, width: u32) -> Self {
89 self.width = Some(width);
90 self
91 }
92
93 pub fn height(mut self, height: u32) -> Self {
94 self.height = Some(height);
95 self
96 }
97
98 pub fn no_wrap(mut self, no_wrap: bool) -> Self {
99 self.no_wrap = no_wrap;
100 self
101 }
102
103 pub fn no_markup(mut self, no_markup: bool) -> Self {
104 self.no_markup = no_markup;
105 self
106 }
107
108 pub fn ellipsize(mut self, ellipsize: bool) -> Self {
109 self.ellipsize = ellipsize;
110 self
111 }
112
113 pub fn switch(mut self, switch: bool) -> Self {
114 self.switch = switch;
115 self
116 }
117
118 pub fn extra_button(mut self, label: &str) -> Self {
119 self.extra_buttons.push(label.to_string());
120 self
121 }
122
123 pub fn show(self) -> Result<DialogResult, Error> {
124 let colors = self.colors.unwrap_or_else(|| crate::ui::detect_theme());
125
126 let temp_font = Font::load(1.0);
128 let mut labels = self.buttons.labels();
129
130 if self.switch {
132 labels = self.extra_buttons.clone();
133 } else {
134 labels.extend(self.extra_buttons.clone());
136 }
137
138 let num_labels = labels.len();
141 labels.reverse();
142 let original_index: Vec<usize> = (0..num_labels).rev().collect();
144
145 let temp_buttons: Vec<Button> = labels
147 .iter()
148 .map(|l| Button::new(l, &temp_font, 1.0))
149 .collect();
150
151 let total_buttons_width: u32 = temp_buttons.iter().map(|b| b.width()).sum::<u32>()
153 + (temp_buttons.len().saturating_sub(1) as u32 * BASE_BUTTON_SPACING);
154
155 let available_width = BASE_MAX_TEXT_WIDTH as u32 + BASE_PADDING * 2;
157 let use_vertical_layout = total_buttons_width > available_width || temp_buttons.len() > 3;
158
159 let logical_buttons_width = if use_vertical_layout {
160 temp_buttons.iter().map(|b| b.width()).max().unwrap_or(0)
162 } else {
163 total_buttons_width
164 };
165
166 let logical_icon_width = if self.icon.is_some() {
167 BASE_ICON_SIZE + BASE_PADDING
168 } else {
169 0
170 };
171
172 let text_width = self.width.map(|w| w as f32).unwrap_or(BASE_MAX_TEXT_WIDTH);
174
175 let temp_text = if self.no_wrap {
177 temp_font.render(&self.text).finish()
178 } else {
179 temp_font
180 .render(&self.text)
181 .with_max_width(text_width)
182 .finish()
183 };
184
185 let logical_content_width = logical_icon_width
188 + if self.no_wrap {
189 temp_text.width().max(text_width as u32)
191 } else {
192 text_width as u32
194 };
195 let logical_inner_width = logical_content_width.max(logical_buttons_width);
196 let calc_width = (logical_inner_width + BASE_PADDING * 2).max(BASE_MIN_WIDTH);
197 let logical_text_height = temp_text.height().max(BASE_ICON_SIZE);
198 let button_area_height = if use_vertical_layout {
199 temp_buttons.len() as u32 * 32
200 + (temp_buttons.len().saturating_sub(1) as u32 * BASE_BUTTON_SPACING)
201 } else {
202 32
203 };
204 let calc_height = BASE_PADDING * 3 + logical_text_height + button_area_height;
205
206 let logical_width = calc_width as u16;
207 let logical_height = self.height.unwrap_or(calc_height) as u16;
208
209 let mut window = create_window(logical_width, logical_height)?;
211 window.set_title(&self.title)?;
212
213 let scale = window.scale_factor();
215
216 let font = Font::load(scale);
218
219 let padding = (BASE_PADDING as f32 * scale) as u32;
221 let button_spacing = (BASE_BUTTON_SPACING as f32 * scale) as u32;
222 let max_text_width = text_width * scale;
223 let button_height = (BASE_BUTTON_HEIGHT as f32 * scale) as u32;
224
225 let mut buttons: Vec<Button> = labels
227 .iter()
228 .map(|l| Button::new(l, &font, scale))
229 .collect();
230
231 let physical_width = (logical_width as f32 * scale) as u32;
233 let physical_height = (logical_height as f32 * scale) as u32;
234
235 let text_canvas = if self.no_wrap {
237 font.render(&self.text).with_color(colors.text).finish()
238 } else {
239 font.render(&self.text)
240 .with_color(colors.text)
241 .with_max_width(max_text_width)
242 .finish()
243 };
244
245 let mut button_positions = Vec::with_capacity(buttons.len());
247
248 if use_vertical_layout {
249 for idx in 0..buttons.len() {
251 let button_y = physical_height as i32
252 - padding as i32
253 - button_height as i32
254 - (idx as i32 * (button_height as i32 + button_spacing as i32));
255
256 let button_x = padding as i32;
258 let button_width = physical_width as i32 - 2 * padding as i32;
259
260 buttons[idx].set_width(button_width as u32);
262 button_positions.push((button_x, button_y));
263 }
264 } else {
265 let mut button_x = physical_width as i32 - padding as i32;
267 for button in buttons.iter().rev() {
268 button_x -= button.width() as i32;
269 let button_y = physical_height as i32 - padding as i32 - button_height as i32;
270 button_positions.push((button_x, button_y));
271 button_x -= button_spacing as i32;
272 }
273 button_positions.reverse();
275 }
276
277 for (idx, button) in buttons.iter_mut().enumerate() {
278 button.set_position(button_positions[idx].0, button_positions[idx].1);
279 }
280
281 let mut canvas = Canvas::new(physical_width, physical_height);
283
284 let icon = self.icon.clone();
286
287 draw_dialog(
289 &mut canvas,
290 colors,
291 &font,
292 &self.text,
293 icon.clone(),
294 &buttons,
295 text_canvas.height(),
296 max_text_width,
297 self.no_wrap,
298 scale,
299 );
300 window.set_contents(&canvas)?;
301 window.show()?;
302
303 let mut dragging = false;
305 let deadline = self
306 .timeout
307 .map(|secs| Instant::now() + Duration::from_secs(secs as u64));
308
309 loop {
310 if let Some(deadline) = deadline {
312 if Instant::now() >= deadline {
313 return Ok(DialogResult::Timeout);
314 }
315 }
316
317 let event = if deadline.is_some() {
319 match window.poll_for_event()? {
320 Some(e) => e,
321 None => {
322 std::thread::sleep(Duration::from_millis(50));
323 continue;
324 }
325 }
326 } else {
327 window.wait_for_event()?
328 };
329
330 match &event {
331 WindowEvent::CloseRequested => {
332 return Ok(DialogResult::Closed);
333 }
334 WindowEvent::RedrawRequested => {
335 draw_dialog(
336 &mut canvas,
337 colors,
338 &font,
339 &self.text,
340 icon.clone(),
341 &buttons,
342 text_canvas.height(),
343 max_text_width,
344 self.no_wrap,
345 scale,
346 );
347 window.set_contents(&canvas)?;
348 }
349 WindowEvent::KeyPress(key_event) => {
350 if key_event.keysym == KEY_ESCAPE {
351 return Ok(DialogResult::Closed);
352 }
353 if key_event.keysym == KEY_RETURN && !buttons.is_empty() {
354 return Ok(DialogResult::Button(0));
355 }
356 }
357 WindowEvent::ButtonPress(MouseButton::Left, _) => {
358 dragging = true;
359 }
360 WindowEvent::ButtonRelease(MouseButton::Left, _) => {
361 if dragging {
362 dragging = false;
363 }
364 }
365 _ => {}
366 }
367
368 let mut needs_redraw = false;
370 for (i, button) in buttons.iter_mut().enumerate() {
371 if button.process_event(&event) {
372 needs_redraw = true;
373 }
374 if button.was_clicked() {
375 return Ok(DialogResult::Button(original_index[i]));
376 }
377 }
378
379 if dragging {
381 if let WindowEvent::CursorMove(_) = &event {
382 let _ = window.start_drag();
383 dragging = false;
384 }
385 }
386
387 while let Some(event) = window.poll_for_event()? {
389 match &event {
390 WindowEvent::CloseRequested => {
391 return Ok(DialogResult::Closed);
392 }
393 _ => {
394 for (i, button) in buttons.iter_mut().enumerate() {
395 if button.process_event(&event) {
396 needs_redraw = true;
397 }
398 if button.was_clicked() {
399 return Ok(DialogResult::Button(original_index[i]));
400 }
401 }
402 }
403 }
404 }
405
406 if needs_redraw {
407 draw_dialog(
408 &mut canvas,
409 colors,
410 &font,
411 &self.text,
412 icon.clone(),
413 &buttons,
414 text_canvas.height(),
415 max_text_width,
416 self.no_wrap,
417 scale,
418 );
419 window.set_contents(&canvas)?;
420 }
421 }
422 }
423}
424
425#[allow(clippy::too_many_arguments)]
426fn draw_dialog(
427 canvas: &mut Canvas,
428 colors: &Colors,
429 font: &Font,
430 text: &str,
431 icon: Option<Icon>,
432 buttons: &[Button],
433 text_height: u32,
434 max_text_width: f32,
435 no_wrap: bool,
436 scale: f32,
437) {
438 let icon_size = (BASE_ICON_SIZE as f32 * scale) as u32;
440 let padding = (BASE_PADDING as f32 * scale) as u32;
441 let width = canvas.width() as f32;
442 let height = canvas.height() as f32;
443 let radius = BASE_CORNER_RADIUS * scale;
444
445 canvas.fill_dialog_bg(
447 width,
448 height,
449 colors.window_bg,
450 colors.window_border,
451 colors.window_shadow,
452 radius,
453 );
454
455 let mut x = padding as i32;
456 let y = padding as i32;
457
458 if let Some(icon) = icon {
460 draw_icon(canvas, x, y, icon, scale);
461 x += (icon_size + padding) as i32;
462 }
463
464 let text_canvas = if no_wrap {
466 font.render(text).with_color(colors.text).finish()
467 } else {
468 font.render(text)
469 .with_color(colors.text)
470 .with_max_width(max_text_width)
471 .finish()
472 };
473
474 let text_x = x + ((max_text_width - text_canvas.width() as f32) / 2.0).max(0.0) as i32;
476 let text_y = y + (icon_size as i32 - text_height as i32) / 2;
478 canvas.draw_canvas(&text_canvas, text_x, text_y.max(y));
479
480 for button in buttons {
482 button.draw_to(canvas, colors, font);
483 }
484}
485
486fn draw_icon(canvas: &mut Canvas, x: i32, y: i32, icon: Icon, scale: f32) {
487 let icon_size = (BASE_ICON_SIZE as f32 * scale) as u32;
488 let inset = 4.0 * scale;
489
490 let (color, shape) = match icon {
491 Icon::Info => (rgb(66, 133, 244), IconShape::Circle),
492 Icon::Warning => (rgb(251, 188, 4), IconShape::Triangle),
493 Icon::Error => (rgb(234, 67, 53), IconShape::Circle),
494 Icon::Question => (rgb(52, 168, 83), IconShape::Circle),
495 Icon::Custom(_) => (rgb(100, 100, 100), IconShape::Circle),
496 };
497
498 let cx = x as f32 + icon_size as f32 / 2.0;
499 let cy = y as f32 + icon_size as f32 / 2.0;
500 let r = icon_size as f32 / 2.0 - (2.0 * scale);
501
502 match shape {
503 IconShape::Circle => {
504 for dy in 0..icon_size {
506 for dx in 0..icon_size {
507 let px = x as f32 + dx as f32 + 0.5;
508 let py = y as f32 + dy as f32 + 0.5;
509 let dist = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
510 if dist <= r {
511 canvas.fill_rect(
512 x as f32 + dx as f32,
513 y as f32 + dy as f32,
514 1.0,
515 1.0,
516 color,
517 );
518 }
519 }
520 }
521 }
522 IconShape::Triangle => {
523 let top = (cx, y as f32 + inset);
525 let left = (x as f32 + inset, y as f32 + icon_size as f32 - inset);
526 let right = (
527 x as f32 + icon_size as f32 - inset,
528 y as f32 + icon_size as f32 - inset,
529 );
530
531 for dy in 0..icon_size {
532 for dx in 0..icon_size {
533 let px = x as f32 + dx as f32 + 0.5;
534 let py = y as f32 + dy as f32 + 0.5;
535 if point_in_triangle(px, py, top, left, right) {
536 canvas.fill_rect(
537 x as f32 + dx as f32,
538 y as f32 + dy as f32,
539 1.0,
540 1.0,
541 color,
542 );
543 }
544 }
545 }
546 }
547 }
548
549 let symbol = match icon {
551 Icon::Info => "i",
552 Icon::Warning => "!",
553 Icon::Error => "X",
554 Icon::Question => "?",
555 Icon::Custom(_) => "i",
556 };
557
558 let font = Font::load(scale);
559 let symbol_canvas = font.render(symbol).with_color(rgb(255, 255, 255)).finish();
560 let sx = x + (icon_size as i32 - symbol_canvas.width() as i32) / 2;
561 let sy = y + (icon_size as i32 - symbol_canvas.height() as i32) / 2;
562 canvas.draw_canvas(&symbol_canvas, sx, sy);
563}
564
565enum IconShape {
566 Circle,
567 Triangle,
568}
569
570fn point_in_triangle(
571 px: f32,
572 py: f32,
573 (ax, ay): (f32, f32),
574 (bx, by): (f32, f32),
575 (cx, cy): (f32, f32),
576) -> bool {
577 let v0x = cx - ax;
578 let v0y = cy - ay;
579 let v1x = bx - ax;
580 let v1y = by - ay;
581 let v2x = px - ax;
582 let v2y = py - ay;
583
584 let dot00 = v0x * v0x + v0y * v0y;
585 let dot01 = v0x * v1x + v0y * v1y;
586 let dot02 = v0x * v2x + v0y * v2y;
587 let dot11 = v1x * v1x + v1y * v1y;
588 let dot12 = v1x * v2x + v1y * v2y;
589
590 let denom = dot00 * dot11 - dot01 * dot01;
591 if denom == 0.0 {
592 return false;
593 }
594 let inv_denom = 1.0 / denom;
595 let u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
596 let v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
597
598 u >= 0.0 && v >= 0.0 && u + v <= 1.0
599}
600
601impl Default for MessageBuilder {
602 fn default() -> Self {
603 Self::new()
604 }
605}