1use ratatui::style::{Color, Style};
2use ratatui::widgets::Block;
3
4use crate::action::{Action, ActionExt};
5use crate::config::OverlayLayoutSettings;
6use crate::ui::{Frame, Rect};
7
8use crate::config::OverlayConfig;
9use crate::utils::Percentage;
10
11#[derive(Debug, Default)]
12pub enum OverlayEffect {
13 #[default]
14 None,
15 Disable,
16}
17
18pub trait Overlay {
19 type A: ActionExt;
20 fn on_enable(&mut self, area: &Rect) {
21 let _ = area;
22 }
23 fn on_disable(&mut self) {}
24 fn handle_input(&mut self, c: char) -> OverlayEffect;
25 fn handle_action(&mut self, action: &Action<Self::A>) -> OverlayEffect {
26 let _ = action;
27 OverlayEffect::None
28 }
29
30 fn draw(&mut self, frame: &mut Frame, area: Rect);
43
44 fn area(&mut self, ui_area: &Rect) -> Result<Rect, [SizeHint; 2]> {
51 let _ = ui_area;
52 Err([0.into(), 0.into()])
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum SizeHint {
59 Min(u16),
60 Max(u16),
61 Exact(u16),
62}
63
64impl From<u16> for SizeHint {
65 fn from(value: u16) -> Self {
66 SizeHint::Exact(value)
67 }
68}
69
70pub struct OverlayUI<A: ActionExt> {
73 overlays: Box<[Box<dyn Overlay<A = A>>]>,
74 index: Option<usize>,
75 config: OverlayConfig,
76 cached_area: Rect,
77}
78
79impl<A: ActionExt> OverlayUI<A> {
80 pub fn new(overlays: Box<[Box<dyn Overlay<A = A>>]>, config: OverlayConfig) -> Self {
81 Self {
82 overlays,
83 index: None,
84 config,
85 cached_area: Default::default(),
86 }
87 }
88
89 pub fn index(&self) -> Option<usize> {
90 self.index
91 }
92
93 pub fn enable(&mut self, index: usize, ui_area: &Rect) {
94 assert!(index < self.overlays.len());
95 self.index = Some(index);
96 self.current_mut().unwrap().on_enable(ui_area);
97 self.update_dimensions(ui_area);
98 }
99
100 pub fn disable(&mut self) {
101 if let Some(x) = self.current_mut() {
102 x.on_disable()
103 }
104 self.index = None
105 }
106
107 pub fn current(&self) -> Option<&dyn Overlay<A = A>> {
108 self.index
109 .and_then(|i| self.overlays.get(i))
110 .map(|b| b.as_ref())
111 }
112
113 fn current_mut(&mut self) -> Option<&mut Box<dyn Overlay<A = A> + 'static>> {
114 if let Some(i) = self.index {
115 self.overlays.get_mut(i)
116 } else {
117 None
118 }
119 }
120
121 pub fn update_dimensions(&mut self, ui_area: &Rect) {
123 if let Some(x) = self.current_mut() {
124 self.cached_area = match x.area(ui_area) {
125 Ok(x) => x,
126 Err(pref) => default_area(pref, &self.config.layout, ui_area),
128 };
129 log::debug!("Overlay area: {}", self.cached_area);
130 }
131 }
132
133 pub fn draw(&mut self, frame: &mut Frame) {
136 let area = self.cached_area;
138 let outer_dim = self.config.outer_dim;
139
140 if let Some(x) = self.current_mut() {
141 if outer_dim {
142 Self::dim_surroundings(frame, area)
143 };
144 x.draw(frame, area);
145 }
146 }
147
148 fn dim_surroundings(frame: &mut Frame, inner: Rect) {
150 let full_area = frame.area();
151 let dim_style = Style::default().bg(Color::Black).fg(Color::DarkGray);
152
153 if inner.y > 0 {
155 let top = Rect {
156 x: 0,
157 y: 0,
158 width: full_area.width,
159 height: inner.y,
160 };
161 frame.render_widget(Block::default().style(dim_style), top);
162 }
163
164 if inner.y + inner.height < full_area.height {
166 let bottom = Rect {
167 x: 0,
168 y: inner.y + inner.height,
169 width: full_area.width,
170 height: full_area.height - (inner.y + inner.height),
171 };
172 frame.render_widget(Block::default().style(dim_style), bottom);
173 }
174
175 if inner.x > 0 {
177 let left = Rect {
178 x: 0,
179 y: inner.y,
180 width: inner.x,
181 height: inner.height,
182 };
183 frame.render_widget(Block::default().style(dim_style), left);
184 }
185
186 if inner.x + inner.width < full_area.width {
188 let right = Rect {
189 x: inner.x + inner.width,
190 y: inner.y,
191 width: full_area.width - (inner.x + inner.width),
192 height: inner.height,
193 };
194 frame.render_widget(Block::default().style(dim_style), right);
195 }
196 }
197
198 pub fn handle_input(&mut self, action: char) -> bool {
200 if let Some(x) = self.current_mut() {
201 match x.handle_input(action) {
202 OverlayEffect::None => {}
203 OverlayEffect::Disable => self.disable(),
204 }
205 true
206 } else {
207 false
208 }
209 }
210
211 pub fn handle_action(&mut self, action: &Action<A>) -> bool {
212 if let Some(inner) = self.current_mut() {
213 match inner.handle_action(action) {
214 OverlayEffect::None => {}
215 OverlayEffect::Disable => self.disable(),
216 }
217 true
218 } else {
219 false
220 }
221 }
222}
223
224pub fn default_area(size: [SizeHint; 2], layout: &OverlayLayoutSettings, ui_area: &Rect) -> Rect {
225 let computed_w =
226 layout.percentage[0].compute_clamped(ui_area.width, layout.min[0], layout.max[0]);
227
228 let computed_h =
229 layout.percentage[1].compute_clamped(ui_area.height, layout.min[1], layout.max[1]);
230
231 let mut w = match size[0] {
232 SizeHint::Exact(v) => v,
233 SizeHint::Min(v) => v.max(computed_w),
234 SizeHint::Max(v) => v.min(computed_w),
235 }
236 .min(ui_area.width);
237
238 let mut h = match size[1] {
239 SizeHint::Exact(v) => v,
240 SizeHint::Min(v) => v.max(computed_h),
241 SizeHint::Max(v) => v.min(computed_h),
242 }
243 .min(ui_area.height);
244
245 if w == 0 && !matches!(size[0], SizeHint::Max(_)) {
246 w = computed_w;
247 }
248 if h == 0 && !matches!(size[1], SizeHint::Max(_)) {
249 h = computed_h;
250 }
251
252 let available_h = ui_area.height.saturating_sub(h);
253 let offset = if layout.y_offset < Percentage::new(50) {
254 let o = layout
255 .y_offset
256 .compute_clamped(available_h.saturating_sub(h), 0, 0);
257
258 (available_h / 2).saturating_sub(o)
259 } else {
260 available_h / 2
261 + layout
262 .y_offset
263 .saturating_sub(50)
264 .compute_clamped(available_h, 0, 0)
265 };
266
267 let x = ui_area.x + (ui_area.width.saturating_sub(w)) / 2;
268 let y = ui_area.y + offset;
269
270 Rect {
271 x,
272 y,
273 width: w,
274 height: h,
275 }
276}
277
278