1use std::any::Any;
6use std::sync::{Arc, Mutex};
7
8use super::component::Component;
9use super::tui::{OverlayAnchor, OverlayOptions, SizeValue};
10use crate::ansi::{bold, dim, visible_width};
11
12pub(crate) struct RenderedOverlay {
13 pub lines: Vec<String>,
14 pub width: usize,
15 pub x: usize,
16 pub y: usize,
17}
18
19pub(crate) fn render_overlay(
22 component: &dyn Component,
23 options: &OverlayOptions,
24 width: usize,
25 height: usize,
26) -> Option<RenderedOverlay> {
27 if width == 0
28 || height == 0
29 || options
30 .visible
31 .is_some_and(|visible| !visible(width, height))
32 {
33 return None;
34 }
35 let margin = options.margin.unwrap_or_default();
36 let available_width = width.saturating_sub(margin.left.saturating_add(margin.right));
37 let available_height = height.saturating_sub(margin.top.saturating_add(margin.bottom));
38 if available_width == 0 || available_height == 0 {
39 return None;
40 }
41 let size = |value: SizeValue, total: usize| match value {
42 SizeValue::Absolute(value) => value,
43 SizeValue::Percent(value) => (total as f64 * value).round() as usize,
44 };
45 let requested_width = options
46 .width
47 .map(|value| size(value, width))
48 .unwrap_or(available_width)
49 .max(options.min_width.unwrap_or(0))
50 .clamp(1, available_width);
51 let mut lines = component.render(requested_width);
52 let overlay_width = if options.width.is_some() {
53 requested_width
54 } else {
55 lines
56 .iter()
57 .map(|row| visible_width(row))
58 .max()
59 .unwrap_or(1)
60 .max(options.min_width.unwrap_or(0))
61 .clamp(1, available_width)
62 };
63 let max_height = options
64 .max_height
65 .map(|value| size(value, height))
66 .unwrap_or(available_height);
67 lines.truncate(max_height.min(available_height));
68 if lines.is_empty() {
69 return None;
70 }
71 let free_x = available_width - overlay_width;
72 let free_y = available_height - lines.len();
73 let x = margin.left
74 + match options.anchor {
75 OverlayAnchor::TopLeft | OverlayAnchor::LeftCenter | OverlayAnchor::BottomLeft => 0,
76 OverlayAnchor::TopRight | OverlayAnchor::RightCenter | OverlayAnchor::BottomRight => {
77 free_x
78 }
79 _ => free_x / 2,
80 };
81 let y = margin.top
82 + match options.anchor {
83 OverlayAnchor::TopLeft | OverlayAnchor::TopCenter | OverlayAnchor::TopRight => 0,
84 OverlayAnchor::BottomLeft
85 | OverlayAnchor::BottomCenter
86 | OverlayAnchor::BottomRight => free_y,
87 _ => free_y / 2,
88 };
89 Some(RenderedOverlay {
90 lines,
91 width: overlay_width,
92 x: x.saturating_add_signed(options.offset_x as isize)
93 .clamp(margin.left, margin.left + free_x),
94 y: y.saturating_add_signed(options.offset_y as isize)
95 .clamp(margin.top, margin.top + free_y),
96 })
97}
98
99pub struct OverlayManager {
101 overlays: Arc<Mutex<Vec<OverlayEntry>>>,
102 next_id: Arc<Mutex<usize>>,
103}
104
105struct OverlayEntry {
106 component: Arc<dyn Component>,
107 options: OverlayOptions,
108 visible: bool,
109 z_index: usize,
110}
111
112impl Clone for OverlayEntry {
114 fn clone(&self) -> Self {
115 Self {
116 component: self.component.clone(),
117 options: self.options.clone(),
118 visible: self.visible,
119 z_index: self.z_index,
120 }
121 }
122}
123
124impl OverlayManager {
125 pub fn new() -> Self {
127 Self {
128 overlays: Arc::new(Mutex::new(Vec::new())),
129 next_id: Arc::new(Mutex::new(0)),
130 }
131 }
132
133 pub fn add(&self, component: Arc<dyn Component>, options: OverlayOptions) -> OverlayHandle {
135 let id = self
136 .next_id
137 .lock()
138 .map(|mut next| {
139 let id = *next;
140 *next += 1;
141 id
142 })
143 .unwrap_or(0);
144 let entry = OverlayEntry {
145 component,
146 options,
147 visible: true,
148 z_index: id,
149 };
150
151 let handle = OverlayHandle {
152 id: entry.z_index,
153 manager: Arc::new(self.clone_manager()),
154 };
155
156 if let Ok(mut overlays) = self.overlays.lock() {
157 overlays.push(entry);
158 }
159
160 handle
161 }
162
163 pub fn remove(&self, id: usize) {
165 if let Ok(mut overlays) = self.overlays.lock() {
166 overlays.retain(|e| e.z_index != id);
167 }
168 }
169
170 pub fn set_visible(&self, id: usize, visible: bool) {
172 if let Ok(mut overlays) = self.overlays.lock() {
173 if let Some(entry) = overlays.iter_mut().find(|e| e.z_index == id) {
174 entry.visible = visible;
175 }
176 }
177 }
178
179 pub fn get_visible(&self) -> Vec<(Arc<dyn Component>, OverlayOptions)> {
181 self.overlays
182 .lock()
183 .map(|overlays| {
184 overlays
185 .iter()
186 .filter(|e| e.visible)
187 .map(|e| (e.component.clone(), e.options.clone()))
188 .collect()
189 })
190 .unwrap_or_default()
191 }
192
193 pub(crate) fn render_visible(&self, width: usize, height: usize) -> Vec<RenderedOverlay> {
196 let mut passive = Vec::<(usize, usize, usize, usize)>::new();
197 let mut rendered = Vec::new();
198 for (component, options) in self.get_visible() {
199 let Some(mut overlay) = render_overlay(component.as_ref(), &options, width, height)
200 else {
201 continue;
202 };
203 if options.non_capturing {
204 let margin = options.margin.unwrap_or_default();
205 let first = margin.top;
206 let last = height - margin.bottom - overlay.lines.len();
207 let y = (first..=last)
208 .filter(|&y| {
209 passive.iter().all(|&(x, top, w, h)| {
210 overlay.x >= x + w
211 || x >= overlay.x + overlay.width
212 || y >= top + h + 1
213 || top >= y + overlay.lines.len() + 1
214 })
215 })
216 .min_by_key(|&y| y.abs_diff(overlay.y));
217 let Some(y) = y else {
218 continue;
219 };
220 overlay.y = y;
221 passive.push((overlay.x, overlay.y, overlay.width, overlay.lines.len()));
222 }
223 rendered.push(overlay);
224 }
225 rendered
226 }
227
228 pub fn remove_topmost(&self) -> bool {
230 let Ok(mut overlays) = self.overlays.lock() else {
231 return false;
232 };
233 let Some(id) = overlays.iter().map(|entry| entry.z_index).max() else {
234 return false;
235 };
236 overlays.retain(|entry| entry.z_index != id);
237 true
238 }
239
240 fn clone_manager(&self) -> Self {
241 Self {
242 overlays: Arc::clone(&self.overlays),
243 next_id: Arc::clone(&self.next_id),
244 }
245 }
246}
247
248impl Clone for OverlayManager {
249 fn clone(&self) -> Self {
250 self.clone_manager()
251 }
252}
253
254impl Default for OverlayManager {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260pub struct OverlayHandle {
262 id: usize,
263 manager: Arc<OverlayManager>,
264}
265
266impl OverlayHandle {
267 pub fn hide(&self) {
269 self.manager.remove(self.id);
270 }
271
272 pub fn set_visible(&self, visible: bool) {
274 self.manager.set_visible(self.id, visible);
275 }
276
277 pub fn is_visible(&self) -> bool {
279 self.manager
280 .overlays
281 .lock()
282 .map(|o| o.iter().any(|e| e.z_index == self.id && e.visible))
283 .unwrap_or(false)
284 }
285}
286
287#[derive(Debug, Clone)]
289pub struct SelectorItem {
290 pub label: String,
291 pub value: String,
292 pub description: Option<String>,
293}
294
295pub struct Selector {
297 items: Mutex<Vec<SelectorItem>>,
298 selected: Mutex<usize>,
299 query: Mutex<String>,
300 padding_x: usize,
301 max_visible: usize,
302 on_select: Mutex<Option<Arc<dyn Fn(&SelectorItem) + Send + Sync>>>,
303}
304
305impl Selector {
306 pub fn new(items: Vec<SelectorItem>, max_visible: usize) -> Self {
308 Self {
309 items: Mutex::new(items),
310 selected: Mutex::new(0),
311 query: Mutex::new(String::new()),
312 padding_x: 1,
313 max_visible,
314 on_select: Mutex::new(None),
315 }
316 }
317
318 pub fn set_items(&self, items: Vec<SelectorItem>) {
320 if let Ok(mut i) = self.items.lock() {
321 *i = items;
322 }
323 }
324
325 pub fn selected(&self) -> usize {
327 self.selected.lock().map(|s| *s).unwrap_or(0)
328 }
329
330 pub fn selected_item(&self) -> Option<SelectorItem> {
332 let items = self.items.lock().ok()?;
333 let selected = self.selected.lock().ok()?;
334 items.get(*selected).cloned()
335 }
336
337 pub fn select_up(&self) {
339 if let Ok(mut selected) = self.selected.lock() {
340 if *selected > 0 {
341 *selected -= 1;
342 }
343 }
344 }
345
346 pub fn select_down(&self) {
348 if let Ok(mut selected) = self.selected.lock() {
349 let items = self.items.lock();
350 if let Ok(items) = items {
351 if *selected < items.len().saturating_sub(1) {
352 *selected += 1;
353 }
354 }
355 }
356 }
357
358 pub fn set_query(&self, query: &str) {
360 if let Ok(mut q) = self.query.lock() {
361 *q = query.to_string();
362 }
363 if let Ok(mut selected) = self.selected.lock() {
365 *selected = 0;
366 }
367 }
368
369 pub fn on_select(&self, callback: Arc<dyn Fn(&SelectorItem) + Send + Sync>) {
371 if let Ok(mut cb) = self.on_select.lock() {
372 *cb = Some(callback);
373 }
374 }
375
376 pub fn confirm(&self) {
378 if let Some(item) = self.selected_item() {
379 if let Ok(cb) = self.on_select.lock() {
380 if let Some(callback) = cb.as_ref() {
381 callback(&item);
382 }
383 }
384 }
385 }
386
387 fn filtered_items(&self) -> Vec<(usize, SelectorItem)> {
389 let items = self.items.lock().map(|i| i.clone()).unwrap_or_default();
390 let query = self
391 .query
392 .lock()
393 .map(|q| q.to_lowercase())
394 .unwrap_or_default();
395
396 if query.is_empty() {
397 return items.into_iter().enumerate().collect();
398 }
399
400 items
401 .into_iter()
402 .enumerate()
403 .filter(|(_, item)| {
404 item.label.to_lowercase().contains(&query)
405 || item.value.to_lowercase().contains(&query)
406 || item
407 .description
408 .as_ref()
409 .map(|d| d.to_lowercase().contains(&query))
410 .unwrap_or(false)
411 })
412 .collect()
413 }
414}
415
416impl Component for Selector {
417 fn render(&self, width: usize) -> Vec<String> {
418 let filtered = self.filtered_items();
419 let mut lines = Vec::new();
420
421 let selected = self.selected.lock().map(|s| *s).unwrap_or(0);
422 let query = self.query.lock().map(|q| q.clone()).unwrap_or_default();
423
424 let mut search_line = " ".repeat(self.padding_x);
426 search_line.push_str(&bold("Search: "));
427 search_line.push_str(&query);
428 lines.push(search_line);
429 lines.push(String::new());
430
431 let visible_count = self.max_visible.min(filtered.len());
433 let scroll_offset = selected.saturating_sub(self.max_visible / 2);
434
435 for (i, (_original_idx, item)) in filtered
436 .iter()
437 .skip(scroll_offset)
438 .take(visible_count)
439 .enumerate()
440 {
441 let is_selected = scroll_offset + i == selected;
442 let mut line = " ".repeat(self.padding_x);
443
444 if is_selected {
445 line.push_str(&format!("\x1b[7m> {}\x1b[0m", item.label));
446 if let Some(desc) = &item.description {
447 line.push_str(&format!(" - {}", desc));
448 }
449 } else {
450 line.push_str(&format!(" {}", item.label));
451 if let Some(desc) = &item.description {
452 line.push_str(&dim(&format!(" - {}", desc)));
453 }
454 }
455
456 if line.len() > width {
458 line = line[..width].to_string();
459 }
460
461 lines.push(line);
462 }
463
464 if filtered.is_empty() {
465 let mut line = " ".repeat(self.padding_x);
466 line.push_str(&dim("No results found"));
467 lines.push(line);
468 }
469
470 lines.push(String::new());
472 let mut footer = " ".repeat(self.padding_x);
473 footer.push_str(&dim(&format!("{} items", filtered.len())));
474 footer.push_str(&dim(" | Enter: Select | Esc: Cancel"));
475 lines.push(footer);
476
477 lines
478 }
479
480 fn invalidate(&self) {
481 }
483
484 fn as_any(&self) -> &dyn Any {
485 self
486 }
487}
488
489pub struct Dialog {
491 title: Mutex<String>,
492 message: Mutex<String>,
493 buttons: Mutex<Vec<String>>,
494 selected: Mutex<usize>,
495}
496
497impl Dialog {
498 pub fn new(title: impl Into<String>, message: impl Into<String>, buttons: Vec<String>) -> Self {
500 Self {
501 title: Mutex::new(title.into()),
502 message: Mutex::new(message.into()),
503 buttons: Mutex::new(buttons),
504 selected: Mutex::new(0),
505 }
506 }
507
508 pub fn info(message: impl Into<String>) -> Self {
510 Self::new("Info", message, vec!["OK".to_string()])
511 }
512
513 pub fn confirm(message: impl Into<String>) -> Self {
515 Self::new(
516 "Confirm",
517 message,
518 vec!["Yes".to_string(), "No".to_string()],
519 )
520 }
521
522 pub fn selected(&self) -> usize {
524 self.selected.lock().map(|s| *s).unwrap_or(0)
525 }
526
527 pub fn select_left(&self) {
529 if let Ok(mut selected) = self.selected.lock() {
530 if *selected > 0 {
531 *selected -= 1;
532 }
533 }
534 }
535
536 pub fn select_right(&self) {
538 if let Ok(mut selected) = self.selected.lock() {
539 let buttons = self.buttons.lock();
540 if let Ok(buttons) = buttons {
541 if *selected < buttons.len().saturating_sub(1) {
542 *selected += 1;
543 }
544 }
545 }
546 }
547}
548
549impl Component for Dialog {
550 fn render(&self, width: usize) -> Vec<String> {
551 let title = self.title.lock().map(|t| t.clone()).unwrap_or_default();
552 let message = self.message.lock().map(|m| m.clone()).unwrap_or_default();
553 let buttons = self.buttons.lock().map(|b| b.clone()).unwrap_or_default();
554 let selected = self.selected.lock().map(|s| *s).unwrap_or(0);
555
556 let mut lines = Vec::new();
557
558 let border_width = width.min(60);
560 lines.push(format!("┌{}┐", "─".repeat(border_width.saturating_sub(2))));
561
562 lines.push(format!(
564 "│ {} {}│",
565 bold(&title),
566 " ".repeat(border_width.saturating_sub(4) - title.len())
567 ));
568
569 lines.push(format!("├{}┤", "─".repeat(border_width.saturating_sub(2))));
571
572 for line in message.lines() {
574 let padded = format!(
575 "│ {}{}",
576 line,
577 " ".repeat(border_width.saturating_sub(3) - visible_width(line))
578 );
579 lines.push(padded);
580 }
581
582 lines.push(format!("├{}┤", "─".repeat(border_width.saturating_sub(2))));
584
585 let mut button_line = "│ ".to_string();
587 for (i, button) in buttons.iter().enumerate() {
588 if i == selected {
589 button_line.push_str(&format!("[{}] ", bold(button)));
590 } else {
591 button_line.push_str(&format!("[{}] ", button));
592 }
593 }
594 button_line
595 .push_str(&" ".repeat(border_width.saturating_sub(3) - visible_width(&button_line)));
596 button_line.push('│');
597 lines.push(button_line);
598
599 lines.push(format!("└{}┘", "─".repeat(border_width.saturating_sub(2))));
601
602 lines
603 }
604
605 fn invalidate(&self) {
606 }
608
609 fn as_any(&self) -> &dyn Any {
610 self
611 }
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
619 fn overlay_wraps_at_allocated_width_and_stays_inside_margins_after_resize() {
620 let content = crate::Markdown::new("one two three four five six", 0, 0);
621 let options = OverlayOptions {
622 width: Some(SizeValue::Percent(0.5)),
623 anchor: OverlayAnchor::BottomRight,
624 offset_x: 1000,
625 offset_y: 1000,
626 margin: Some(super::super::tui::OverlayMargin {
627 bottom: 3,
628 ..Default::default()
629 }),
630 ..Default::default()
631 };
632 for width in [12, 20, 60] {
633 let overlay = render_overlay(&content, &options, width, 10).unwrap();
634 assert_eq!(overlay.width, width / 2);
635 assert!(overlay
636 .lines
637 .iter()
638 .all(|row| visible_width(row) <= overlay.width));
639 assert!(overlay.x + overlay.width <= width);
640 assert!(overlay.y + overlay.lines.len() <= 7);
641 if width == 20 {
642 assert!(
643 overlay.lines.len() > 1,
644 "wrap instead of clipping a full-width line"
645 );
646 }
647 }
648 let options = OverlayOptions {
649 offset_x: -1000,
650 offset_y: -1000,
651 ..options
652 };
653 let overlay = render_overlay(&content, &options, 20, 10).unwrap();
654 assert_eq!((overlay.x, overlay.y), (0, 0));
655 assert!(render_overlay(&content, &options, 20, 3).is_none());
656 }
657
658 #[test]
659 fn passive_panels_avoid_each_other_and_reappear_when_space_returns() {
660 let manager = OverlayManager::new();
661 let options = OverlayOptions {
662 anchor: OverlayAnchor::BottomRight,
663 width: Some(SizeValue::Absolute(12)),
664 non_capturing: true,
665 ..Default::default()
666 };
667 let _a = manager.add(
668 Arc::new(crate::Text::new("first\nsecond", 0, 0)),
669 options.clone(),
670 );
671 let _b = manager.add(
672 Arc::new(crate::Text::new("other\nlast", 0, 0)),
673 options.clone(),
674 );
675 let rendered = manager.render_visible(40, 8);
676 assert_eq!(rendered.len(), 2);
677 assert_eq!(rendered[0].y, 6);
678 assert_eq!(rendered[1].y, 3);
679 assert_eq!(manager.render_visible(40, 4).len(), 1);
680 assert_eq!(manager.render_visible(40, 8).len(), 2);
681 let _modal = manager.add(
683 Arc::new(crate::Text::new("dialog", 0, 0)),
684 OverlayOptions {
685 non_capturing: false,
686 ..options
687 },
688 );
689 assert_eq!(manager.render_visible(40, 8).last().unwrap().y, 7);
690 }
691
692 #[test]
693 fn test_selector() {
694 let items = vec![
695 SelectorItem {
696 label: "Item 1".into(),
697 value: "1".into(),
698 description: None,
699 },
700 SelectorItem {
701 label: "Item 2".into(),
702 value: "2".into(),
703 description: None,
704 },
705 ];
706 let selector = Selector::new(items, 5);
707 assert_eq!(selector.selected(), 0);
708 selector.select_down();
709 assert_eq!(selector.selected(), 1);
710 }
711
712 #[test]
713 fn test_dialog() {
714 let dialog = Dialog::info("Hello, World!");
715 let lines = dialog.render(40);
716 assert!(!lines.is_empty());
717 }
718}