1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Overlay compositing + input routing. Run with
//! `cargo run --example overlay` (o open · enter/esc close · q quit).
//!
//! Demonstrates a [`SceneOverlay`] following a laid-out trigger through a
//! [`RectProbe`](tuika::probe::RectProbe). The preferred below-target placement
//! flips above when the terminal edge leaves more room there. While the popover
//! is open it owns input — the same key means different things by overlay state.
use std::io;
use std::time::Duration;
use crossterm::event::{self};
use ratatui::backend::CrosstermBackend;
use ratatui::text::{Line, Span};
use ratatui::{Terminal, TerminalOptions, Viewport};
use tuika::prelude::*;
use tuika::probe::RectProbe;
fn main() -> io::Result<()> {
let mut open = false;
let mut confirmed = 0u32;
let trigger = RectProbe::new();
let _session = TerminalSession::enter()?;
let mut terminal = Terminal::with_options(
CrosstermBackend::new(io::stdout()),
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)?;
let theme = Theme::default();
loop {
terminal.draw(|f| {
let area = f.area();
let hint = if open {
"popover open — enter = confirm · esc = dismiss"
} else {
"o open popover · q quit"
};
let trigger_view = trigger.wrap(Text::new(vec![Line::from(Span::styled(
"[ o Open actions ]",
theme.accent_style(),
))]));
let base: Element = view! {
col(padding = Padding::all(1), gap = 1) {
node(Text::new(vec![Line::from(Span::styled(
"tuika overlay demo",
theme.accent_style(),
))]))
fixed(4) {
boxed(title = Line::from(" base layer ")) {
node(Text::raw(format!(
"confirmed {confirmed} time(s) — base content stays visible around the panel"
)))
}
}
grow(1) { spacer() }
fixed(1) {
row {
grow(1) { spacer() }
fixed(18) { node(trigger_view) }
}
}
fixed(1) {
node(Text::new(vec![Line::from(Span::styled(hint, theme.muted_style()))]))
}
}
};
let mut scene = Scene::new(base);
if open {
let popover = view! {
boxed(
title = Line::from(Span::styled(" actions ", theme.accent_style())),
padding = Padding::all(1)
) {
col(gap = 1) {
node(Text::raw("Proceed with the action?"))
node(Text::new(vec![Line::from(Span::styled(
"enter = yes · esc = no",
theme.muted_style(),
))]))
}
}
};
let spec = OverlaySpec {
width: tuika::overlay::Extent::Cells(38),
height: tuika::overlay::Extent::Cells(7),
..OverlaySpec::centered(0, 0).margin(1)
};
scene = scene.overlay(
SceneOverlay::new(popover, spec)
.target(
&trigger,
TargetPlacement::below().align(TargetAlign::End).gap(1),
)
.focus_owner("actions"),
);
}
paint_scene(f.buffer_mut(), area, &theme, &scene);
})?;
if !event::poll(Duration::from_millis(150))? {
continue;
}
let Some(Event::Key(k)) = translate_event(event::read()?) else {
continue;
};
if !k.plain() {
continue;
}
match (open, k.code) {
(false, KeyCode::Char('q')) => break,
(false, KeyCode::Char('o')) => open = true,
(true, KeyCode::Enter) => {
confirmed += 1;
open = false;
}
(true, KeyCode::Esc) => open = false,
_ => {}
}
}
let _ = terminal.clear();
drop(terminal);
Ok(())
}