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;
9
10#[derive(Debug, Default)]
11pub enum OverlayEffect {
12    #[default]
13    None,
14    Disable,
15}
16
17pub trait Overlay {
18    type A: ActionExt;
19    fn on_enable(&mut self, area: &Rect) {
20        let _ = area;
21    }
22    fn on_disable(&mut self) {}
23    fn handle_input(&mut self, c: char) -> OverlayEffect;
24    fn handle_action(&mut self, action: &Action<Self::A>) -> OverlayEffect {
25        let _ = action;
26        OverlayEffect::None
27    }
28
29    // methods are mutable for flexibility (i.e. render_stateful_widget)
30
31    /// Draw the widget within the rect
32    ///
33    /// # Example
34    /// ```rust
35    //  pub fn draw(&self, frame: &mut Frame, area: Rect) {
36    //      let widget = self.make_widget();
37    //      frame.render_widget(Clear, area);
38    //      frame.render_widget(widget, area);
39    // }
40    /// ```
41    fn draw(&mut self, frame: &mut Frame, area: Rect);
42
43    /// Called when layout area changes.
44    /// The output of this is processed and cached into the area which the draw method is called with.
45    ///
46    /// # Notes
47    /// Return None or Err([0, 0]) to draw in the default area (see [`OverlayConfig`] and [`default_area`])
48    fn area(&mut self, ui_area: &Rect) -> Result<Rect, [u16; 2]> {
49        let _ = ui_area;
50        Err([0, 0])
51    }
52}
53
54// -------- OVERLAY_UI -----------
55
56pub struct OverlayUI<A: ActionExt> {
57    overlays: Box<[Box<dyn Overlay<A = A>>]>,
58    index: Option<usize>,
59    config: OverlayConfig,
60    cached_area: Rect,
61}
62
63impl<A: ActionExt> OverlayUI<A> {
64    pub fn new(overlays: Box<[Box<dyn Overlay<A = A>>]>, config: OverlayConfig) -> Self {
65        Self {
66            overlays,
67            index: None,
68            config,
69            cached_area: Default::default(),
70        }
71    }
72
73    pub fn index(&self) -> Option<usize> {
74        self.index
75    }
76
77    pub fn enable(&mut self, index: usize, ui_area: &Rect) {
78        assert!(index < self.overlays.len());
79        self.index = Some(index);
80        self.current_mut().unwrap().on_enable(ui_area);
81        self.update_dimensions(ui_area);
82    }
83
84    pub fn disable(&mut self) {
85        if let Some(x) = self.current_mut() {
86            x.on_disable()
87        }
88        self.index = None
89    }
90
91    pub fn current(&self) -> Option<&dyn Overlay<A = A>> {
92        self.index
93            .and_then(|i| self.overlays.get(i))
94            .map(|b| b.as_ref())
95    }
96
97    fn current_mut(&mut self) -> Option<&mut Box<dyn Overlay<A = A> + 'static>> {
98        if let Some(i) = self.index {
99            self.overlays.get_mut(i)
100        } else {
101            None
102        }
103    }
104
105    // ---------
106    pub fn update_dimensions(&mut self, ui_area: &Rect) {
107        if let Some(x) = self.current_mut() {
108            self.cached_area = match x.area(ui_area) {
109                Ok(x) => x,
110                // centered
111                Err(pref) => default_area(pref, &self.config.layout, ui_area),
112            };
113            log::debug!("Overlay area: {}", self.cached_area);
114        }
115    }
116
117    // -----------
118
119    pub fn draw(&mut self, frame: &mut Frame) {
120        // Draw the overlay on top
121        let area = self.cached_area;
122        let outer_dim = self.config.outer_dim;
123
124        if let Some(x) = self.current_mut() {
125            if outer_dim {
126                Self::dim_surroundings(frame, area)
127            };
128            x.draw(frame, area);
129        }
130    }
131
132    // todo: bottom is missing + looks bad
133    fn dim_surroundings(frame: &mut Frame, inner: Rect) {
134        let full_area = frame.area();
135        let dim_style = Style::default().bg(Color::Black).fg(Color::DarkGray);
136
137        // Top
138        if inner.y > 0 {
139            let top = Rect {
140                x: 0,
141                y: 0,
142                width: full_area.width,
143                height: inner.y,
144            };
145            frame.render_widget(Block::default().style(dim_style), top);
146        }
147
148        // Bottom
149        if inner.y + inner.height < full_area.height {
150            let bottom = Rect {
151                x: 0,
152                y: inner.y + inner.height,
153                width: full_area.width,
154                height: full_area.height - (inner.y + inner.height),
155            };
156            frame.render_widget(Block::default().style(dim_style), bottom);
157        }
158
159        // Left
160        if inner.x > 0 {
161            let left = Rect {
162                x: 0,
163                y: inner.y,
164                width: inner.x,
165                height: inner.height,
166            };
167            frame.render_widget(Block::default().style(dim_style), left);
168        }
169
170        // Right
171        if inner.x + inner.width < full_area.width {
172            let right = Rect {
173                x: inner.x + inner.width,
174                y: inner.y,
175                width: full_area.width - (inner.x + inner.width),
176                height: inner.height,
177            };
178            frame.render_widget(Block::default().style(dim_style), right);
179        }
180    }
181
182    /// Returns whether the overlay was active (handled the action)
183    pub fn handle_input(&mut self, action: char) -> bool {
184        if let Some(x) = self.current_mut() {
185            match x.handle_input(action) {
186                OverlayEffect::None => {}
187                OverlayEffect::Disable => self.disable(),
188            }
189            true
190        } else {
191            false
192        }
193    }
194
195    pub fn handle_action(&mut self, action: &Action<A>) -> bool {
196        if let Some(inner) = self.current_mut() {
197            match inner.handle_action(action) {
198                OverlayEffect::None => {}
199                OverlayEffect::Disable => self.disable(),
200            }
201            true
202        } else {
203            false
204        }
205    }
206}
207
208pub fn default_area(
209    [mut w, mut h]: [u16; 2],
210    layout: &OverlayLayoutSettings,
211    ui_area: &Rect,
212) -> Rect {
213    // compute preferred size from percentage then clamp to min/max
214    if w == 0 {
215        w = layout.percentage[0].compute_clamped(ui_area.width, layout.min[0], layout.max[0]);
216    }
217    if h == 0 {
218        h = layout.percentage[1].compute_clamped(ui_area.height, layout.min[1], layout.max[1]);
219        h = h.clamp(layout.min[1], layout.max[1]);
220    }
221
222    // center within ui_area
223    let x = ui_area.x + (ui_area.width.saturating_sub(w)) / 2;
224    let y = ui_area.y + (ui_area.height.saturating_sub(h + 8)) / 2; // bump up 4 lines nearer to top
225
226    Rect {
227        x,
228        y,
229        width: w,
230        height: h,
231    }
232}
233
234// ------------------------
235// would be cool if associated types could be recovered from erased traits
236// I think this can be done by wrapping overlay with a fn turning make_widget into draw
237// type Widget: ratatui::widgets::Widget;
238// fn make_widget(&self) -> Self::Widget {
239//     todo!()
240// }
241// // OverlayUI
242// pub fn draw(&self, frame: &mut Frame) {
243//     if let Some(overlay) = &self.inner {
244//         let widget = overlay.make_widget();
245//         frame.render_widget(widget, overlay.area());
246//     }
247// }