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