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}
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    // methods are mutable for flexibility (i.e. render_stateful_widget)
31
32    /// Draw the widget within the rect
33    ///
34    /// # Example
35    /// ```rust
36    //  pub fn draw(&self, frame: &mut Frame, area: Rect) {
37    //      let widget = self.make_widget();
38    //      frame.render_widget(Clear, area);
39    //      frame.render_widget(widget, area);
40    // }
41    /// ```
42    fn draw(&mut self, frame: &mut Frame, area: Rect);
43
44    /// Called when layout area changes.
45    /// The output of this is processed and cached into the area which the draw method is called with.
46    ///
47    /// # Returns
48    /// - Ok: The Rect to render in
49    /// - Err: a [`SizeHint`] used to compute the area to render in
50    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/// If Exact(0), the default computed dimension is used (see [`OverlayConfig`] and [`default_area`]).
57#[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
70// -------- OVERLAY_UI -----------
71
72pub 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    // ---------
122    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                // centered
127                Err(pref) => default_area(pref, &self.config.layout, ui_area),
128            };
129            log::debug!("Overlay area: {}", self.cached_area);
130        }
131    }
132
133    // -----------
134
135    pub fn draw(&mut self, frame: &mut Frame) {
136        // Draw the overlay on top
137        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    // todo: bottom is missing + looks bad
149    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        // Top
154        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        // Bottom
165        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        // Left
176        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        // Right
187        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    /// Returns whether the overlay was active (handled the action)
199    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// ------------------------
279// would be cool if associated types could be recovered from erased traits
280// I think this can be done by wrapping overlay with a fn turning make_widget into draw
281// type Widget: ratatui::widgets::Widget;
282// fn make_widget(&self) -> Self::Widget {
283//     todo!()
284// }
285// // OverlayUI
286// pub fn draw(&self, frame: &mut Frame) {
287//     if let Some(overlay) = &self.inner {
288//         let widget = overlay.make_widget();
289//         frame.render_widget(widget, overlay.area());
290//     }
291// }