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