1use crate::compositor::Layer;
7use crate::geometry::{Position, Rect, Size};
8use crate::segment::Segment;
9use crate::style::Style;
10
11pub type OverlayId = u64;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Placement {
17 Above,
19 Below,
21 Left,
23 Right,
25}
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum OverlayPosition {
30 Center,
32 At(Position),
34 Anchored {
36 anchor: Rect,
38 placement: Placement,
40 },
41}
42
43#[derive(Debug, Clone)]
45pub struct OverlayConfig {
46 pub position: OverlayPosition,
48 pub size: Size,
50 pub z_offset: i32,
52 pub dim_background: bool,
54}
55
56struct OverlayEntry {
57 id: OverlayId,
58 config: OverlayConfig,
59 lines: Vec<Vec<Segment>>,
60}
61
62pub struct ScreenStack {
68 overlays: Vec<OverlayEntry>,
69 next_id: OverlayId,
70 base_z: i32,
71}
72
73impl ScreenStack {
74 pub fn new() -> Self {
76 Self {
77 overlays: Vec::new(),
78 next_id: 1,
79 base_z: 1000,
80 }
81 }
82
83 pub fn push(&mut self, config: OverlayConfig, lines: Vec<Vec<Segment>>) -> OverlayId {
87 let id = self.next_id;
88 self.next_id += 1;
89 self.overlays.push(OverlayEntry { id, config, lines });
90 id
91 }
92
93 pub fn pop(&mut self) -> Option<OverlayId> {
95 self.overlays.pop().map(|e| e.id)
96 }
97
98 pub fn remove(&mut self, id: OverlayId) -> bool {
102 let before = self.overlays.len();
103 self.overlays.retain(|e| e.id != id);
104 self.overlays.len() < before
105 }
106
107 pub fn clear(&mut self) {
109 self.overlays.clear();
110 }
111
112 pub fn len(&self) -> usize {
114 self.overlays.len()
115 }
116
117 pub fn is_empty(&self) -> bool {
119 self.overlays.is_empty()
120 }
121
122 pub fn resolve_position(position: &OverlayPosition, size: Size, screen: Size) -> Position {
124 match position {
125 OverlayPosition::Center => {
126 let x = screen.width.saturating_sub(size.width) / 2;
127 let y = screen.height.saturating_sub(size.height) / 2;
128 Position::new(x, y)
129 }
130 OverlayPosition::At(pos) => *pos,
131 OverlayPosition::Anchored { anchor, placement } => match placement {
132 Placement::Above => {
133 let x = anchor
134 .position
135 .x
136 .saturating_add(anchor.size.width / 2)
137 .saturating_sub(size.width / 2);
138 let y = anchor.position.y.saturating_sub(size.height);
139 Position::new(x, y)
140 }
141 Placement::Below => {
142 let x = anchor
143 .position
144 .x
145 .saturating_add(anchor.size.width / 2)
146 .saturating_sub(size.width / 2);
147 let y = anchor.position.y.saturating_add(anchor.size.height);
148 Position::new(x, y)
149 }
150 Placement::Left => {
151 let x = anchor.position.x.saturating_sub(size.width);
152 let y = anchor
153 .position
154 .y
155 .saturating_add(anchor.size.height / 2)
156 .saturating_sub(size.height / 2);
157 Position::new(x, y)
158 }
159 Placement::Right => {
160 let x = anchor.position.x.saturating_add(anchor.size.width);
161 let y = anchor
162 .position
163 .y
164 .saturating_add(anchor.size.height / 2)
165 .saturating_sub(size.height / 2);
166 Position::new(x, y)
167 }
168 },
169 }
170 }
171
172 pub fn apply_to_compositor(
178 &self,
179 compositor: &mut crate::compositor::Compositor,
180 screen: Size,
181 ) {
182 for (i, entry) in self.overlays.iter().enumerate() {
183 let z = self.base_z + (i as i32) * 10 + entry.config.z_offset;
184
185 if entry.config.dim_background {
186 compositor.add_layer(create_dim_layer(screen, z - 1));
187 }
188
189 let pos = Self::resolve_position(&entry.config.position, entry.config.size, screen);
190 let region = Rect::new(
191 pos.x,
192 pos.y,
193 entry.config.size.width,
194 entry.config.size.height,
195 );
196 compositor.add_layer(Layer::new(entry.id, region, z, entry.lines.clone()));
197 }
198 }
199}
200
201impl Default for ScreenStack {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207pub fn create_dim_layer(screen: Size, z_index: i32) -> Layer {
209 let dim_style = Style::new().dim(true);
210 let mut lines = Vec::new();
211 for _ in 0..screen.height {
212 lines.push(vec![Segment::styled(
213 " ".repeat(screen.width as usize),
214 dim_style.clone(),
215 )]);
216 }
217 Layer::new(
218 0,
219 Rect::new(0, 0, screen.width, screen.height),
220 z_index,
221 lines,
222 )
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn empty_stack() {
231 let stack = ScreenStack::new();
232 assert!(stack.is_empty());
233 assert!(stack.is_empty());
234 }
235
236 #[test]
237 fn push_increments_len() {
238 let mut stack = ScreenStack::new();
239 let config = OverlayConfig {
240 position: OverlayPosition::Center,
241 size: Size::new(10, 5),
242 z_offset: 0,
243 dim_background: false,
244 };
245 let id = stack.push(config, vec![vec![Segment::new("hi")]]);
246 assert!(id == 1);
247 assert!(stack.len() == 1);
248 }
249
250 #[test]
251 fn pop_returns_topmost() {
252 let mut stack = ScreenStack::new();
253 let config = OverlayConfig {
254 position: OverlayPosition::Center,
255 size: Size::new(10, 5),
256 z_offset: 0,
257 dim_background: false,
258 };
259 let _id1 = stack.push(config.clone(), vec![]);
260 let id2 = stack.push(config, vec![]);
261 assert!(stack.pop() == Some(id2));
262 assert!(stack.len() == 1);
263 }
264
265 #[test]
266 fn pop_empty_returns_none() {
267 let mut stack = ScreenStack::new();
268 assert!(stack.pop().is_none());
269 }
270
271 #[test]
272 fn remove_by_id() {
273 let mut stack = ScreenStack::new();
274 let config = OverlayConfig {
275 position: OverlayPosition::Center,
276 size: Size::new(10, 5),
277 z_offset: 0,
278 dim_background: false,
279 };
280 let id1 = stack.push(config.clone(), vec![]);
281 let _id2 = stack.push(config, vec![]);
282 assert!(stack.remove(id1));
283 assert!(stack.len() == 1);
284 }
285
286 #[test]
287 fn remove_nonexistent_returns_false() {
288 let mut stack = ScreenStack::new();
289 assert!(!stack.remove(999));
290 }
291
292 #[test]
293 fn clear_removes_all() {
294 let mut stack = ScreenStack::new();
295 let config = OverlayConfig {
296 position: OverlayPosition::Center,
297 size: Size::new(10, 5),
298 z_offset: 0,
299 dim_background: false,
300 };
301 stack.push(config.clone(), vec![]);
302 stack.push(config, vec![]);
303 stack.clear();
304 assert!(stack.is_empty());
305 }
306
307 #[test]
308 fn resolve_center() {
309 let pos = ScreenStack::resolve_position(
310 &OverlayPosition::Center,
311 Size::new(20, 10),
312 Size::new(80, 24),
313 );
314 assert!(pos.x == 30);
315 assert!(pos.y == 7);
316 }
317
318 #[test]
319 fn resolve_at() {
320 let pos = ScreenStack::resolve_position(
321 &OverlayPosition::At(Position::new(5, 3)),
322 Size::new(20, 10),
323 Size::new(80, 24),
324 );
325 assert!(pos.x == 5);
326 assert!(pos.y == 3);
327 }
328
329 #[test]
330 fn resolve_anchored_below() {
331 let anchor = Rect::new(30, 5, 10, 2);
332 let pos = ScreenStack::resolve_position(
333 &OverlayPosition::Anchored {
334 anchor,
335 placement: Placement::Below,
336 },
337 Size::new(20, 3),
338 Size::new(80, 24),
339 );
340 assert!(pos.x == 25);
342 assert!(pos.y == 7);
344 }
345
346 #[test]
347 fn resolve_anchored_above() {
348 let anchor = Rect::new(30, 10, 10, 2);
349 let pos = ScreenStack::resolve_position(
350 &OverlayPosition::Anchored {
351 anchor,
352 placement: Placement::Above,
353 },
354 Size::new(20, 3),
355 Size::new(80, 24),
356 );
357 assert!(pos.x == 25);
358 assert!(pos.y == 7); }
360
361 #[test]
362 fn resolve_anchored_right() {
363 let anchor = Rect::new(10, 10, 5, 4);
364 let pos = ScreenStack::resolve_position(
365 &OverlayPosition::Anchored {
366 anchor,
367 placement: Placement::Right,
368 },
369 Size::new(8, 3),
370 Size::new(80, 24),
371 );
372 assert!(pos.x == 15); assert!(pos.y == 11); }
375
376 #[test]
377 fn dim_layer_covers_screen() {
378 let layer = create_dim_layer(Size::new(80, 24), 999);
379 assert!(layer.z_index == 999);
380 assert!(layer.region.size.width == 80);
381 assert!(layer.region.size.height == 24);
382 assert!(layer.lines.len() == 24);
383 }
384
385 #[test]
386 fn dim_layer_style_is_dim() {
387 let layer = create_dim_layer(Size::new(10, 2), 500);
388 assert!(layer.lines.len() == 2);
389 assert!(layer.lines[0][0].style.dim);
390 }
391
392 #[test]
393 fn apply_to_compositor_adds_layers() {
394 let mut stack = ScreenStack::new();
395 let config = OverlayConfig {
396 position: OverlayPosition::Center,
397 size: Size::new(10, 3),
398 z_offset: 0,
399 dim_background: false,
400 };
401 stack.push(config, vec![vec![Segment::new("test")]]);
402
403 let mut compositor = crate::compositor::Compositor::new(80, 24);
404 stack.apply_to_compositor(&mut compositor, Size::new(80, 24));
405
406 let mut buf = crate::buffer::ScreenBuffer::new(Size::new(80, 24));
407 compositor.compose(&mut buf);
408
409 match buf.get(35, 10) {
411 Some(cell) => assert!(cell.grapheme == "t"),
412 None => unreachable!(),
413 }
414 }
415
416 #[test]
417 fn apply_with_dim_background() {
418 let mut stack = ScreenStack::new();
419 let config = OverlayConfig {
420 position: OverlayPosition::At(Position::new(5, 5)),
421 size: Size::new(10, 3),
422 z_offset: 0,
423 dim_background: true,
424 };
425 stack.push(config, vec![vec![Segment::new("modal")]]);
426
427 let mut compositor = crate::compositor::Compositor::new(80, 24);
428 stack.apply_to_compositor(&mut compositor, Size::new(80, 24));
429
430 let mut buf = crate::buffer::ScreenBuffer::new(Size::new(80, 24));
431 compositor.compose(&mut buf);
432
433 match buf.get(0, 0) {
435 Some(cell) => assert!(cell.style.dim),
436 None => unreachable!(),
437 }
438 match buf.get(5, 5) {
440 Some(cell) => assert!(cell.grapheme == "m"),
441 None => unreachable!(),
442 }
443 }
444
445 #[test]
448 fn modal_centered_on_screen() {
449 use crate::widget::modal::Modal;
450
451 let modal = Modal::new("Test", 20, 5);
452 let lines = modal.render_to_lines();
453 let config = modal.to_overlay_config();
454
455 let mut stack = ScreenStack::new();
456 stack.push(config, lines);
457
458 let screen = Size::new(80, 24);
459 let mut compositor = crate::compositor::Compositor::new(80, 24);
460 stack.apply_to_compositor(&mut compositor, screen);
461
462 let mut buf = crate::buffer::ScreenBuffer::new(screen);
463 compositor.compose(&mut buf);
464
465 match buf.get(30, 9) {
468 Some(cell) => assert!(cell.grapheme == "┌"),
469 None => unreachable!(),
470 }
471 }
472
473 #[test]
474 fn modal_with_dim_background_pipeline() {
475 use crate::widget::modal::Modal;
476
477 let modal = Modal::new("Dim", 20, 5);
478 let lines = modal.render_to_lines();
479 let config = modal.to_overlay_config();
480 assert!(config.dim_background);
482
483 let mut stack = ScreenStack::new();
484 stack.push(config, lines);
485
486 let screen = Size::new(80, 24);
487 let mut compositor = crate::compositor::Compositor::new(80, 24);
488 stack.apply_to_compositor(&mut compositor, screen);
489
490 let mut buf = crate::buffer::ScreenBuffer::new(screen);
491 compositor.compose(&mut buf);
492
493 match buf.get(0, 0) {
495 Some(cell) => assert!(cell.style.dim),
496 None => unreachable!(),
497 }
498 }
499
500 #[test]
501 fn toast_at_top_right_pipeline() {
502 use crate::widget::toast::Toast;
503
504 let toast = Toast::new("Saved!").with_width(10);
505 let lines = toast.render_to_lines();
506 let screen = Size::new(80, 24);
507 let config = toast.to_overlay_config(screen);
508
509 let mut stack = ScreenStack::new();
510 stack.push(config, lines);
511
512 let mut compositor = crate::compositor::Compositor::new(80, 24);
513 stack.apply_to_compositor(&mut compositor, screen);
514
515 let mut buf = crate::buffer::ScreenBuffer::new(screen);
516 compositor.compose(&mut buf);
517
518 match buf.get(70, 0) {
520 Some(cell) => assert!(cell.grapheme == "S"),
521 None => unreachable!(),
522 }
523 }
524
525 #[test]
526 fn tooltip_below_anchor_pipeline() {
527 use crate::overlay::Placement;
528 use crate::widget::tooltip::Tooltip;
529
530 let anchor = Rect::new(30, 5, 10, 2);
531 let tooltip = Tooltip::new("hint", anchor).with_placement(Placement::Below);
532 let lines = tooltip.render_to_lines();
533 let screen = Size::new(80, 24);
534 let config = tooltip.to_overlay_config(screen);
535
536 let mut stack = ScreenStack::new();
537 stack.push(config, lines);
538
539 let mut compositor = crate::compositor::Compositor::new(80, 24);
540 stack.apply_to_compositor(&mut compositor, screen);
541
542 let mut buf = crate::buffer::ScreenBuffer::new(screen);
543 compositor.compose(&mut buf);
544
545 match buf.get(33, 7) {
547 Some(cell) => assert!(cell.grapheme == "h"),
548 None => unreachable!(),
549 }
550 }
551
552 #[test]
553 fn two_modals_stacked() {
554 let mut stack = ScreenStack::new();
555
556 let config1 = OverlayConfig {
558 position: OverlayPosition::At(Position::new(10, 5)),
559 size: Size::new(10, 3),
560 z_offset: 0,
561 dim_background: false,
562 };
563 stack.push(config1, vec![vec![Segment::new("first")]]);
564
565 let config2 = OverlayConfig {
567 position: OverlayPosition::At(Position::new(10, 5)),
568 size: Size::new(10, 3),
569 z_offset: 0,
570 dim_background: false,
571 };
572 stack.push(config2, vec![vec![Segment::new("second")]]);
573
574 let screen = Size::new(80, 24);
575 let mut compositor = crate::compositor::Compositor::new(80, 24);
576 stack.apply_to_compositor(&mut compositor, screen);
577
578 let mut buf = crate::buffer::ScreenBuffer::new(screen);
579 compositor.compose(&mut buf);
580
581 match buf.get(10, 5) {
583 Some(cell) => assert!(cell.grapheme == "s"),
584 None => unreachable!(),
585 }
586 }
587
588 #[test]
589 fn modal_plus_toast_z_order() {
590 use crate::widget::modal::Modal;
591 use crate::widget::toast::Toast;
592
593 let modal = Modal::new("M", 20, 5);
594 let modal_lines = modal.render_to_lines();
595 let modal_config = modal.to_overlay_config();
596
597 let toast = Toast::new("Toast!").with_width(10);
598 let screen = Size::new(80, 24);
599 let toast_lines = toast.render_to_lines();
600 let toast_config = toast.to_overlay_config(screen);
601
602 let mut stack = ScreenStack::new();
603 stack.push(modal_config, modal_lines);
604 stack.push(toast_config, toast_lines);
605
606 let mut compositor = crate::compositor::Compositor::new(80, 24);
607 stack.apply_to_compositor(&mut compositor, screen);
608
609 let mut buf = crate::buffer::ScreenBuffer::new(screen);
610 compositor.compose(&mut buf);
611
612 match buf.get(70, 0) {
614 Some(cell) => assert!(cell.grapheme == "T"),
615 None => unreachable!(),
616 }
617 }
618
619 #[test]
620 fn remove_modal_clears_dim() {
621 let mut stack = ScreenStack::new();
622 let config = OverlayConfig {
623 position: OverlayPosition::Center,
624 size: Size::new(10, 3),
625 z_offset: 0,
626 dim_background: true,
627 };
628 let id = stack.push(config, vec![vec![Segment::new("x")]]);
629
630 assert!(stack.remove(id));
632 assert!(stack.is_empty());
633
634 let screen = Size::new(80, 24);
635 let mut compositor = crate::compositor::Compositor::new(80, 24);
636 stack.apply_to_compositor(&mut compositor, screen);
637
638 let mut buf = crate::buffer::ScreenBuffer::new(screen);
639 compositor.compose(&mut buf);
640
641 match buf.get(0, 0) {
643 Some(cell) => assert!(!cell.style.dim),
644 None => unreachable!(),
645 }
646 }
647
648 #[test]
649 fn clear_removes_all_overlays() {
650 let mut stack = ScreenStack::new();
651 let config = OverlayConfig {
652 position: OverlayPosition::At(Position::new(0, 0)),
653 size: Size::new(5, 1),
654 z_offset: 0,
655 dim_background: false,
656 };
657 stack.push(config.clone(), vec![vec![Segment::new("A")]]);
658 stack.push(config, vec![vec![Segment::new("B")]]);
659 stack.clear();
660
661 let screen = Size::new(80, 24);
662 let mut compositor = crate::compositor::Compositor::new(80, 24);
663 stack.apply_to_compositor(&mut compositor, screen);
664
665 let mut buf = crate::buffer::ScreenBuffer::new(screen);
666 compositor.compose(&mut buf);
667
668 match buf.get(0, 0) {
670 Some(cell) => assert!(cell.grapheme == " "),
671 None => unreachable!(),
672 }
673 }
674}