rat_popup/
popup.rs

1use crate::_private::NonExhaustive;
2use crate::event::PopupOutcome;
3use crate::{Placement, PopupConstraint};
4use rat_event::util::MouseFlags;
5use rat_event::{HandleEvent, Popup, ct_event};
6use rat_reloc::RelocatableState;
7use ratatui_core::buffer::Buffer;
8use ratatui_core::layout::{Alignment, Rect};
9use ratatui_core::style::Style;
10use ratatui_core::widgets::StatefulWidget;
11use ratatui_crossterm::crossterm::event::Event;
12use std::cell::Cell;
13use std::cmp::max;
14use std::rc::Rc;
15
16/// Provides the core for popup widgets.
17///
18/// This widget can calculate the placement of a popup widget
19/// using [placement](PopupCore::constraint), [offset](PopupCore::offset)
20/// and the outer [boundary](PopupCore::boundary).
21///
22/// It provides the widget area as [area](PopupCoreState::area).
23///
24/// After rendering the PopupCore the main widget can render it's
25/// content in the calculated [PopupCoreState::area].
26///
27/// ## Event handling
28///
29/// Will detect any mouse-clicks outside its area and
30/// return [PopupOutcome::Hide]. Actually showing/hiding the popup is
31/// the job of the main widget.
32///
33/// __See__
34/// See the examples some variants.
35///
36#[derive(Debug, Clone)]
37pub struct PopupCore {
38    /// Constraints for the popup.
39    pub constraint: Cell<PopupConstraint>,
40    /// Extra offset after calculating the position
41    /// with constraint.
42    pub offset: (i16, i16),
43    /// Outer boundary for the popup-placement.
44    /// If not set uses the buffer-area.
45    pub boundary_area: Option<Rect>,
46
47    pub non_exhaustive: NonExhaustive,
48}
49
50/// Complete styles for the popup.
51#[derive(Debug, Clone)]
52pub struct PopupStyle {
53    /// Extra offset added after applying the constraints.
54    pub offset: Option<(i16, i16)>,
55    /// Alignment.
56    pub alignment: Option<Alignment>,
57    /// Placement
58    pub placement: Option<Placement>,
59
60    /// non-exhaustive struct.
61    pub non_exhaustive: NonExhaustive,
62}
63
64/// State for the PopupCore.
65#[derive(Debug)]
66pub struct PopupCoreState {
67    /// Area for the widget.
68    /// This is the area given to render(), corrected by the
69    /// given constraints.
70    /// __read only__. renewed for each render.
71    pub area: Rect,
72    /// Z-Index for the popup.
73    pub area_z: u16,
74
75    /// Active flag for the popup.
76    ///
77    /// __read+write__
78    pub active: Rc<Cell<bool>>,
79
80    /// Mouse flags.
81    /// __read+write__
82    pub mouse: MouseFlags,
83
84    /// non-exhaustive struct.
85    pub non_exhaustive: NonExhaustive,
86}
87
88impl Default for PopupCore {
89    fn default() -> Self {
90        Self {
91            constraint: Cell::new(PopupConstraint::None),
92            offset: (0, 0),
93            boundary_area: None,
94            non_exhaustive: NonExhaustive,
95        }
96    }
97}
98
99impl PopupCore {
100    /// New.
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Placement constraints for the popup widget.
106    pub fn ref_constraint(&self, constraint: PopupConstraint) -> &Self {
107        self.constraint.set(constraint);
108        self
109    }
110
111    /// Placement constraints for the popup widget.
112    pub fn constraint(self, constraint: PopupConstraint) -> Self {
113        self.constraint.set(constraint);
114        self
115    }
116
117    /// Adds an extra offset to the widget area.
118    ///
119    /// This can be used to
120    /// * place the widget under the mouse cursor.
121    /// * align the widget not by the outer bounds but by
122    ///   the text content.
123    pub fn offset(mut self, offset: (i16, i16)) -> Self {
124        self.offset = offset;
125        self
126    }
127
128    /// Sets only the x offset.
129    /// See [offset](Self::offset)
130    pub fn x_offset(mut self, offset: i16) -> Self {
131        self.offset.0 = offset;
132        self
133    }
134
135    /// Sets only the y offset.
136    /// See [offset](Self::offset)
137    pub fn y_offset(mut self, offset: i16) -> Self {
138        self.offset.1 = offset;
139        self
140    }
141
142    /// Sets outer boundaries for the popup widget.
143    ///
144    /// This will be used to ensure that the popup widget is fully visible.
145    /// First it tries to move the popup in a way that is fully inside
146    /// this area. If this is not enought the popup area will be clipped.
147    ///
148    /// If this is not set, [Buffer::area] will be used instead.
149    pub fn boundary(mut self, boundary: Rect) -> Self {
150        self.boundary_area = Some(boundary);
151        self
152    }
153
154    /// Set styles
155    pub fn styles(mut self, styles: PopupStyle) -> Self {
156        if let Some(offset) = styles.offset {
157            self.offset = offset;
158        }
159
160        self
161    }
162
163    /// Run the layout to calculate the popup area before rendering.
164    pub fn layout(&self, area: Rect, buf: &Buffer) -> Rect {
165        self._layout(area, self.boundary_area.unwrap_or(buf.area))
166    }
167}
168
169impl StatefulWidget for &PopupCore {
170    type State = PopupCoreState;
171
172    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
173        render_popup(self, area, buf, state);
174    }
175}
176
177impl StatefulWidget for PopupCore {
178    type State = PopupCoreState;
179
180    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
181        render_popup(&self, area, buf, state);
182    }
183}
184
185fn render_popup(widget: &PopupCore, area: Rect, buf: &mut Buffer, state: &mut PopupCoreState) {
186    if !state.active.get() {
187        state.clear_areas();
188        return;
189    }
190
191    state.area = widget._layout(area, widget.boundary_area.unwrap_or(buf.area));
192
193    reset_buf_area(state.area, buf);
194}
195
196/// Fallback for popup style.
197pub fn fallback_popup_style(style: Style) -> Style {
198    if style.fg.is_some() || style.bg.is_some() {
199        style
200    } else {
201        style.black().on_gray()
202    }
203}
204
205/// Reset an area of the buffer.
206pub fn reset_buf_area(area: Rect, buf: &mut Buffer) {
207    for y in area.top()..area.bottom() {
208        for x in area.left()..area.right() {
209            if let Some(cell) = buf.cell_mut((x, y)) {
210                cell.reset();
211            }
212        }
213    }
214}
215
216impl PopupCore {
217    fn _layout(&self, area: Rect, boundary_area: Rect) -> Rect {
218        // helper fn
219        fn center(len: u16, within: u16) -> u16 {
220            ((within as i32 - len as i32) / 2).clamp(0, i16::MAX as i32) as u16
221        }
222        let middle = center;
223        fn right(len: u16, within: u16) -> u16 {
224            within.saturating_sub(len)
225        }
226        let bottom = right;
227
228        // offsets may change
229        let mut offset = self.offset;
230
231        let mut area = match self.constraint.get() {
232            PopupConstraint::None => area,
233            PopupConstraint::Above(Alignment::Left, rel) => Rect::new(
234                rel.x,
235                rel.y.saturating_sub(area.height),
236                area.width,
237                area.height,
238            ),
239            PopupConstraint::Above(Alignment::Center, rel) => Rect::new(
240                rel.x + center(area.width, rel.width),
241                rel.y.saturating_sub(area.height),
242                area.width,
243                area.height,
244            ),
245            PopupConstraint::Above(Alignment::Right, rel) => Rect::new(
246                rel.x + right(area.width, rel.width),
247                rel.y.saturating_sub(area.height),
248                area.width,
249                area.height,
250            ),
251            PopupConstraint::Below(Alignment::Left, rel) => Rect::new(
252                rel.x, //
253                rel.bottom(),
254                area.width,
255                area.height,
256            ),
257            PopupConstraint::Below(Alignment::Center, rel) => Rect::new(
258                rel.x + center(area.width, rel.width),
259                rel.bottom(),
260                area.width,
261                area.height,
262            ),
263            PopupConstraint::Below(Alignment::Right, rel) => Rect::new(
264                rel.x + right(area.width, rel.width),
265                rel.bottom(),
266                area.width,
267                area.height,
268            ),
269
270            PopupConstraint::Left(Alignment::Left, rel) => Rect::new(
271                rel.x.saturating_sub(area.width),
272                rel.y,
273                area.width,
274                area.height,
275            ),
276            PopupConstraint::Left(Alignment::Center, rel) => Rect::new(
277                rel.x.saturating_sub(area.width),
278                rel.y + middle(area.height, rel.height),
279                area.width,
280                area.height,
281            ),
282            PopupConstraint::Left(Alignment::Right, rel) => Rect::new(
283                rel.x.saturating_sub(area.width),
284                rel.y + bottom(area.height, rel.height),
285                area.width,
286                area.height,
287            ),
288            PopupConstraint::Right(Alignment::Left, rel) => Rect::new(
289                rel.right(), //
290                rel.y,
291                area.width,
292                area.height,
293            ),
294            PopupConstraint::Right(Alignment::Center, rel) => Rect::new(
295                rel.right(),
296                rel.y + middle(area.height, rel.height),
297                area.width,
298                area.height,
299            ),
300            PopupConstraint::Right(Alignment::Right, rel) => Rect::new(
301                rel.right(),
302                rel.y + bottom(area.height, rel.height),
303                area.width,
304                area.height,
305            ),
306
307            PopupConstraint::Position(x, y) => Rect::new(
308                x, //
309                y,
310                area.width,
311                area.height,
312            ),
313
314            PopupConstraint::AboveOrBelow(Alignment::Left, rel) => {
315                if area.height.saturating_add_signed(-self.offset.1) < rel.y {
316                    Rect::new(
317                        rel.x,
318                        rel.y.saturating_sub(area.height),
319                        area.width,
320                        area.height,
321                    )
322                } else {
323                    offset = (offset.0, -offset.1);
324                    Rect::new(
325                        rel.x, //
326                        rel.bottom(),
327                        area.width,
328                        area.height,
329                    )
330                }
331            }
332            PopupConstraint::AboveOrBelow(Alignment::Center, rel) => {
333                if area.height.saturating_add_signed(-self.offset.1) < rel.y {
334                    Rect::new(
335                        rel.x + center(area.width, rel.width),
336                        rel.y.saturating_sub(area.height),
337                        area.width,
338                        area.height,
339                    )
340                } else {
341                    offset = (offset.0, -offset.1);
342                    Rect::new(
343                        rel.x + center(area.width, rel.width), //
344                        rel.bottom(),
345                        area.width,
346                        area.height,
347                    )
348                }
349            }
350            PopupConstraint::AboveOrBelow(Alignment::Right, rel) => {
351                if area.height.saturating_add_signed(-self.offset.1) < rel.y {
352                    Rect::new(
353                        rel.x + right(area.width, rel.width),
354                        rel.y.saturating_sub(area.height),
355                        area.width,
356                        area.height,
357                    )
358                } else {
359                    offset = (offset.0, -offset.1);
360                    Rect::new(
361                        rel.x + right(area.width, rel.width), //
362                        rel.bottom(),
363                        area.width,
364                        area.height,
365                    )
366                }
367            }
368            PopupConstraint::BelowOrAbove(Alignment::Left, rel) => {
369                if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
370                    <= boundary_area.height
371                {
372                    Rect::new(
373                        rel.x, //
374                        rel.bottom(),
375                        area.width,
376                        area.height,
377                    )
378                } else {
379                    offset = (offset.0, -offset.1);
380                    Rect::new(
381                        rel.x,
382                        rel.y.saturating_sub(area.height),
383                        area.width,
384                        area.height,
385                    )
386                }
387            }
388            PopupConstraint::BelowOrAbove(Alignment::Center, rel) => {
389                if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
390                    <= boundary_area.height
391                {
392                    Rect::new(
393                        rel.x + center(area.width, rel.width), //
394                        rel.bottom(),
395                        area.width,
396                        area.height,
397                    )
398                } else {
399                    offset = (offset.0, -offset.1);
400                    Rect::new(
401                        rel.x + center(area.width, rel.width),
402                        rel.y.saturating_sub(area.height),
403                        area.width,
404                        area.height,
405                    )
406                }
407            }
408            PopupConstraint::BelowOrAbove(Alignment::Right, rel) => {
409                if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
410                    <= boundary_area.height
411                {
412                    Rect::new(
413                        rel.x + right(area.width, rel.width), //
414                        rel.bottom(),
415                        area.width,
416                        area.height,
417                    )
418                } else {
419                    offset = (offset.0, -offset.1);
420                    Rect::new(
421                        rel.x + right(area.width, rel.width),
422                        rel.y.saturating_sub(area.height),
423                        area.width,
424                        area.height,
425                    )
426                }
427            }
428        };
429
430        // offset
431        area.x = area.x.saturating_add_signed(offset.0);
432        area.y = area.y.saturating_add_signed(offset.1);
433
434        // keep in sight
435        if area.left() < boundary_area.left() {
436            area.x = boundary_area.left();
437        }
438        if area.right() >= boundary_area.right() {
439            let corr = area.right().saturating_sub(boundary_area.right());
440            area.x = max(boundary_area.left(), area.x.saturating_sub(corr));
441        }
442        if area.top() < boundary_area.top() {
443            area.y = boundary_area.top();
444        }
445        if area.bottom() >= boundary_area.bottom() {
446            let corr = area.bottom().saturating_sub(boundary_area.bottom());
447            area.y = max(boundary_area.top(), area.y.saturating_sub(corr));
448        }
449
450        // shrink to size
451        if area.right() > boundary_area.right() {
452            let corr = area.right() - boundary_area.right();
453            area.width = area.width.saturating_sub(corr);
454        }
455        if area.bottom() > boundary_area.bottom() {
456            let corr = area.bottom() - boundary_area.bottom();
457            area.height = area.height.saturating_sub(corr);
458        }
459
460        area
461    }
462}
463
464impl Default for PopupStyle {
465    fn default() -> Self {
466        Self {
467            offset: None,
468            alignment: None,
469            placement: None,
470            non_exhaustive: NonExhaustive,
471        }
472    }
473}
474
475impl Clone for PopupCoreState {
476    fn clone(&self) -> Self {
477        Self {
478            area: self.area,
479            area_z: self.area_z,
480            active: self.active.clone(),
481            mouse: Default::default(),
482            non_exhaustive: NonExhaustive,
483        }
484    }
485}
486
487impl Default for PopupCoreState {
488    fn default() -> Self {
489        Self {
490            area: Default::default(),
491            area_z: 1,
492            active: Default::default(),
493            mouse: Default::default(),
494            non_exhaustive: NonExhaustive,
495        }
496    }
497}
498
499impl RelocatableState for PopupCoreState {
500    fn relocate(&mut self, _shift: (i16, i16), _clip: Rect) {}
501
502    fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
503        self.area.relocate(shift, clip);
504    }
505}
506
507impl PopupCoreState {
508    /// New
509    #[inline]
510    pub fn new() -> Self {
511        Default::default()
512    }
513
514    /// Is the popup active/visible.
515    pub fn is_active(&self) -> bool {
516        self.active.get()
517    }
518
519    /// Flip visibility of the popup.
520    pub fn flip_active(&mut self) {
521        self.set_active(!self.is_active());
522    }
523
524    /// Show the popup.
525    /// This will set gained/lost flags according to the change.
526    /// If the popup is hidden this will clear all flags.
527    pub fn set_active(&mut self, active: bool) -> bool {
528        let old_value = self.is_active();
529        self.active.set(active);
530        old_value != self.is_active()
531    }
532
533    /// Clear all stored areas.
534    pub fn clear_areas(&mut self) {
535        self.area = Default::default();
536    }
537}
538
539impl HandleEvent<Event, Popup, PopupOutcome> for PopupCoreState {
540    fn handle(&mut self, event: &Event, _qualifier: Popup) -> PopupOutcome {
541        if self.is_active() {
542            match event {
543                ct_event!(mouse down Left for x,y)
544                | ct_event!(mouse down Right for x,y)
545                | ct_event!(mouse down Middle for x,y)
546                    if !self.area.contains((*x, *y).into()) =>
547                {
548                    PopupOutcome::Hide
549                }
550                _ => PopupOutcome::Continue,
551            }
552        } else {
553            PopupOutcome::Continue
554        }
555    }
556}