Skip to main content

matchmaker/ui/
overlay.rs

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    // methods are mutable for flexibility (i.e. render_stateful_widget)
32
33    /// Draw the widget within the rect
34    ///
35    /// # Example
36    /// ```rust
37    //  pub fn draw(&self, frame: &mut Frame, area: Rect) {
38    //      let widget = self.make_widget();
39    //      frame.render_widget(Clear, area);
40    //      frame.render_widget(widget, area);
41    // }
42    /// ```
43    fn draw(&mut self, frame: &mut Frame, area: Rect);
44
45    /// Called when layout area changes.
46    /// The output of this is processed and cached into the area which the draw method is called with.
47    ///
48    /// # Returns
49    /// - Ok: The Rect to render in
50    /// - Err: a [`SizeHint`] used to compute the area to render in
51    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/// If Exact(0), the default computed dimension is used (see [`OverlayConfig`] and [`default_area`]).
58#[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
71// -------- OVERLAY_UI -----------
72
73pub 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    // ---------
123    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                // centered
128                Err(pref) => default_area(pref, &self.config.layout, ui_area),
129            };
130            log::debug!("Overlay area: {}", self.cached_area);
131        }
132    }
133
134    // -----------
135
136    pub fn draw(&mut self, frame: &mut Frame) {
137        // Draw the overlay on top
138        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    // todo: bottom is missing + looks bad
150    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        // Top
155        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        // Bottom
166        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        // Left
177        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        // Right
188        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    /// Returns whether the overlay was active (handled the action)
200    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        // recenter
238        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// ------------------------
298// would be cool if associated types could be recovered from erased traits
299// I think this can be done by wrapping overlay with a fn turning make_widget into draw
300// type Widget: ratatui::widgets::Widget;
301// fn make_widget(&self) -> Self::Widget {
302//     todo!()
303// }
304// // OverlayUI
305// pub fn draw(&self, frame: &mut Frame) {
306//     if let Some(overlay) = &self.inner {
307//         let widget = overlay.make_widget();
308//         frame.render_widget(widget, overlay.area());
309//     }
310// }