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_focus::FocusFlag;
7use rat_reloc::{RelocatableState, relocate_area};
8use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
9use ratatui::buffer::Buffer;
10use ratatui::layout::{Alignment, Rect, Size};
11use ratatui::prelude::BlockExt;
12use ratatui::style::{Style, Stylize};
13use ratatui::widgets::{Block, Padding, StatefulWidget};
14use std::cell::Cell;
15use std::cmp::max;
16
17/// Provides the core for popup widgets.
18///
19/// This widget can calculate the placement of a popup widget
20/// using [placement](PopupCore::constraint), [offset](PopupCore::offset)
21/// and the outer [boundary](PopupCore::boundary).
22///
23/// It provides the widget area as [area](PopupCoreState::area).
24///
25/// After rendering the PopupCore the main widget can render it's
26/// content in the calculated [PopupCoreState::area].
27///
28/// ## Event handling
29///
30/// Will detect any mouse-clicks outside its area and
31/// return [PopupOutcome::Hide]. Actually showing/hiding the popup is
32/// the job of the main widget.
33///
34/// __See__
35/// See the examples some variants.
36///
37#[derive(Debug, Clone)]
38pub struct PopupCore<'a> {
39    #[deprecated(since = "1.2.0", note = "job for the main widget")]
40    pub style: Style,
41
42    /// Constraints for the popup.
43    pub constraint: Cell<PopupConstraint>,
44    /// Extra offset after calculating the position
45    /// with constraint.
46    pub offset: (i16, i16),
47    /// Outer boundary for the popup-placement.
48    /// If not set uses the buffer-area.
49    pub boundary_area: Option<Rect>,
50
51    #[deprecated(since = "1.2.0", note = "job for the main widget")]
52    pub block: Option<Block<'a>>,
53    #[deprecated(since = "1.2.0", note = "job for the main widget")]
54    pub h_scroll: Option<Scroll<'a>>,
55    #[deprecated(since = "1.2.0", note = "job for the main widget")]
56    pub v_scroll: Option<Scroll<'a>>,
57
58    pub non_exhaustive: NonExhaustive,
59}
60
61/// Complete styles for the popup.
62#[derive(Debug, Clone)]
63pub struct PopupStyle {
64    /// Baseline style.
65    #[deprecated(since = "1.2.0", note = "job for the main widget")]
66    pub style: Style,
67    /// Extra offset added after applying the constraints.
68    pub offset: Option<(i16, i16)>,
69    /// Block for the popup.
70    #[deprecated(since = "1.2.0", note = "job for the main widget")]
71    pub block: Option<Block<'static>>,
72    /// Style for the block border.
73    #[deprecated(since = "1.2.0", note = "job for the main widget")]
74    pub border_style: Option<Style>,
75    /// Style for scroll bars.
76    #[deprecated(since = "1.2.0", note = "job for the main widget")]
77    pub scroll: Option<ScrollStyle>,
78    /// Alignment.
79    pub alignment: Option<Alignment>,
80    /// Placement
81    pub placement: Option<Placement>,
82
83    /// non-exhaustive struct.
84    pub non_exhaustive: NonExhaustive,
85}
86
87/// State for the PopupCore.
88#[derive(Debug)]
89pub struct PopupCoreState {
90    /// Area for the widget.
91    /// This is the area given to render(), corrected by the
92    /// given constraints.
93    /// __read only__. renewed for each render.
94    pub area: Rect,
95    /// Z-Index for the popup.
96    pub area_z: u16,
97    /// Area where the widget can render it's content.
98    /// __read only__. renewed for each render.
99    #[deprecated(since = "1.2.0", note = "use area instead")]
100    pub widget_area: Rect,
101
102    /// Horizontal scroll state if active.
103    /// __read+write__
104    #[deprecated(since = "1.2.0", note = "job for the main widget")]
105    pub h_scroll: ScrollState,
106    /// Vertical scroll state if active.
107    /// __read+write__
108    #[deprecated(since = "1.2.0", note = "job for the main widget")]
109    pub v_scroll: ScrollState,
110
111    /// Active flag for the popup.
112    ///
113    /// __Note__
114    ///
115    /// This will change to a bool in the future. Use [is_active](PopupCoreState::is_active)
116    /// and [set_active](PopupCoreState::set_active) to future-proof.
117    ///
118    /// __read+write__
119    #[deprecated(
120        since = "1.0.2",
121        note = "use is_active() and set_active() instead. will change type."
122    )]
123    pub active: FocusFlag,
124
125    /// Mouse flags.
126    /// __read+write__
127    pub mouse: MouseFlags,
128
129    /// non-exhaustive struct.
130    pub non_exhaustive: NonExhaustive,
131}
132
133impl Default for PopupCore<'_> {
134    #[allow(deprecated)]
135    fn default() -> Self {
136        Self {
137            style: Default::default(),
138            constraint: Cell::new(PopupConstraint::None),
139            offset: (0, 0),
140            boundary_area: None,
141            block: None,
142            h_scroll: None,
143            v_scroll: None,
144            non_exhaustive: NonExhaustive,
145        }
146    }
147}
148
149impl<'a> PopupCore<'a> {
150    /// New.
151    pub fn new() -> Self {
152        Self::default()
153    }
154
155    /// Placement constraints for the popup widget.
156    pub fn ref_constraint(&self, constraint: PopupConstraint) -> &Self {
157        self.constraint.set(constraint);
158        self
159    }
160
161    /// Placement constraints for the popup widget.
162    pub fn constraint(self, constraint: PopupConstraint) -> Self {
163        self.constraint.set(constraint);
164        self
165    }
166
167    /// Adds an extra offset to the widget area.
168    ///
169    /// This can be used to
170    /// * place the widget under the mouse cursor.
171    /// * align the widget not by the outer bounds but by
172    ///   the text content.
173    pub fn offset(mut self, offset: (i16, i16)) -> Self {
174        self.offset = offset;
175        self
176    }
177
178    /// Sets only the x offset.
179    /// See [offset](Self::offset)
180    pub fn x_offset(mut self, offset: i16) -> Self {
181        self.offset.0 = offset;
182        self
183    }
184
185    /// Sets only the y offset.
186    /// See [offset](Self::offset)
187    pub fn y_offset(mut self, offset: i16) -> Self {
188        self.offset.1 = offset;
189        self
190    }
191
192    /// Sets outer boundaries for the popup widget.
193    ///
194    /// This will be used to ensure that the popup widget is fully visible.
195    /// First it tries to move the popup in a way that is fully inside
196    /// this area. If this is not enought the popup area will be clipped.
197    ///
198    /// If this is not set, [Buffer::area] will be used instead.
199    pub fn boundary(mut self, boundary: Rect) -> Self {
200        self.boundary_area = Some(boundary);
201        self
202    }
203
204    /// Set styles
205    #[allow(deprecated)]
206    pub fn styles(mut self, styles: PopupStyle) -> Self {
207        self.style = styles.style;
208        if let Some(offset) = styles.offset {
209            self.offset = offset;
210        }
211        self.block = self.block.map(|v| v.style(self.style));
212        if let Some(border_style) = styles.border_style {
213            self.block = self.block.map(|v| v.border_style(border_style));
214        }
215        if let Some(block) = styles.block {
216            self.block = Some(block);
217        }
218        if let Some(styles) = styles.scroll {
219            if let Some(h_scroll) = self.h_scroll {
220                self.h_scroll = Some(h_scroll.styles(styles.clone()));
221            }
222            if let Some(v_scroll) = self.v_scroll {
223                self.v_scroll = Some(v_scroll.styles(styles));
224            }
225        }
226
227        self
228    }
229
230    /// Base style for the popup.
231    #[allow(deprecated)]
232    #[deprecated(since = "1.2.0", note = "job for the main widget")]
233    pub fn style(mut self, style: Style) -> Self {
234        self.style = style;
235        self.block = self.block.map(|v| v.style(self.style));
236        self
237    }
238
239    /// Block
240    #[allow(deprecated)]
241    #[deprecated(since = "1.2.0", note = "job for the main widget")]
242    pub fn block(mut self, block: Block<'a>) -> Self {
243        self.block = Some(block);
244        self.block = self.block.map(|v| v.style(self.style));
245        self
246    }
247
248    /// Block
249    #[allow(deprecated)]
250    #[deprecated(since = "1.2.0", note = "job for the main widget")]
251    pub fn block_opt(mut self, block: Option<Block<'a>>) -> Self {
252        self.block = block;
253        self.block = self.block.map(|v| v.style(self.style));
254        self
255    }
256
257    /// Horizontal scroll
258    #[allow(deprecated)]
259    #[deprecated(since = "1.2.0", note = "job for the main widget")]
260    pub fn h_scroll(mut self, h_scroll: Scroll<'a>) -> Self {
261        self.h_scroll = Some(h_scroll);
262        self
263    }
264
265    /// Horizontal scroll
266    #[allow(deprecated)]
267    #[deprecated(since = "1.2.0", note = "job for the main widget")]
268    pub fn h_scroll_opt(mut self, h_scroll: Option<Scroll<'a>>) -> Self {
269        self.h_scroll = h_scroll;
270        self
271    }
272
273    /// Vertical scroll
274    #[allow(deprecated)]
275    #[deprecated(since = "1.2.0", note = "job for the main widget")]
276    pub fn v_scroll(mut self, v_scroll: Scroll<'a>) -> Self {
277        self.v_scroll = Some(v_scroll);
278        self
279    }
280
281    /// Vertical scroll
282    #[allow(deprecated)]
283    #[deprecated(since = "1.2.0", note = "job for the main widget")]
284    pub fn v_scroll_opt(mut self, v_scroll: Option<Scroll<'a>>) -> Self {
285        self.v_scroll = v_scroll;
286        self
287    }
288
289    /// Get the padding the block imposes as  Size.
290    #[allow(deprecated)]
291    #[deprecated(since = "1.2.0", note = "job for the main widget")]
292    pub fn get_block_size(&self) -> Size {
293        let area = Rect::new(0, 0, 20, 20);
294        let inner = self.block.inner_if_some(area);
295        Size {
296            width: (inner.left() - area.left()) + (area.right() - inner.right()),
297            height: (inner.top() - area.top()) + (area.bottom() - inner.bottom()),
298        }
299    }
300
301    /// Get the padding the block imposes as Padding.
302    #[allow(deprecated)]
303    #[deprecated(since = "1.2.0", note = "job for the main widget")]
304    pub fn get_block_padding(&self) -> Padding {
305        let area = Rect::new(0, 0, 20, 20);
306        let inner = self.block.inner_if_some(area);
307        Padding {
308            left: inner.left() - area.left(),
309            right: area.right() - inner.right(),
310            top: inner.top() - area.top(),
311            bottom: area.bottom() - inner.bottom(),
312        }
313    }
314
315    /// Calculate the inner area.
316    #[allow(deprecated)]
317    #[deprecated(since = "1.2.0", note = "job for the main widget")]
318    pub fn inner(&self, area: Rect) -> Rect {
319        self.block.inner_if_some(area)
320    }
321
322    /// Run the layout to calculate the popup area before rendering.
323    pub fn layout(&self, area: Rect, buf: &Buffer) -> Rect {
324        self._layout(area, self.boundary_area.unwrap_or(buf.area))
325    }
326}
327
328impl<'a> StatefulWidget for &'a PopupCore<'a> {
329    type State = PopupCoreState;
330
331    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
332        render_popup(self, area, buf, state);
333    }
334}
335
336impl StatefulWidget for PopupCore<'_> {
337    type State = PopupCoreState;
338
339    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
340        render_popup(&self, area, buf, state);
341    }
342}
343
344#[allow(deprecated)]
345fn render_popup(widget: &PopupCore<'_>, area: Rect, buf: &mut Buffer, state: &mut PopupCoreState) {
346    if !state.active.get() {
347        state.clear_areas();
348        return;
349    }
350
351    state.area = widget._layout(area, widget.boundary_area.unwrap_or(buf.area));
352
353    reset_buf_area(state.area, buf);
354
355    if widget.block.is_some() || widget.h_scroll.is_some() || widget.v_scroll.is_some() {
356        let sa = ScrollArea::new()
357            .block(widget.block.as_ref())
358            .h_scroll(widget.h_scroll.as_ref())
359            .v_scroll(widget.v_scroll.as_ref())
360            .style(fallback_popup_style(widget.style));
361
362        state.widget_area = sa.inner(state.area, Some(&state.h_scroll), Some(&state.v_scroll));
363
364        sa.render(
365            state.area,
366            buf,
367            &mut ScrollAreaState::new()
368                .h_scroll(&mut state.h_scroll)
369                .v_scroll(&mut state.v_scroll),
370        );
371    } else {
372        state.widget_area = state.area;
373    }
374}
375
376/// Fallback for popup style.
377pub fn fallback_popup_style(style: Style) -> Style {
378    if style.fg.is_some() || style.bg.is_some() {
379        style
380    } else {
381        style.black().on_gray()
382    }
383}
384
385/// Reset an area of the buffer.
386pub fn reset_buf_area(area: Rect, buf: &mut Buffer) {
387    for y in area.top()..area.bottom() {
388        for x in area.left()..area.right() {
389            if let Some(cell) = buf.cell_mut((x, y)) {
390                cell.reset();
391            }
392        }
393    }
394}
395
396impl PopupCore<'_> {
397    fn _layout(&self, area: Rect, boundary_area: Rect) -> Rect {
398        // helper fn
399        fn center(len: u16, within: u16) -> u16 {
400            ((within as i32 - len as i32) / 2).clamp(0, i16::MAX as i32) as u16
401        }
402        let middle = center;
403        fn right(len: u16, within: u16) -> u16 {
404            within.saturating_sub(len)
405        }
406        let bottom = right;
407
408        // offsets may change
409        let mut offset = self.offset;
410
411        let mut area = match self.constraint.get() {
412            PopupConstraint::None => area,
413            PopupConstraint::Above(Alignment::Left, rel) => Rect::new(
414                rel.x,
415                rel.y.saturating_sub(area.height),
416                area.width,
417                area.height,
418            ),
419            PopupConstraint::Above(Alignment::Center, rel) => Rect::new(
420                rel.x + center(area.width, rel.width),
421                rel.y.saturating_sub(area.height),
422                area.width,
423                area.height,
424            ),
425            PopupConstraint::Above(Alignment::Right, rel) => Rect::new(
426                rel.x + right(area.width, rel.width),
427                rel.y.saturating_sub(area.height),
428                area.width,
429                area.height,
430            ),
431            PopupConstraint::Below(Alignment::Left, rel) => Rect::new(
432                rel.x, //
433                rel.bottom(),
434                area.width,
435                area.height,
436            ),
437            PopupConstraint::Below(Alignment::Center, rel) => Rect::new(
438                rel.x + center(area.width, rel.width),
439                rel.bottom(),
440                area.width,
441                area.height,
442            ),
443            PopupConstraint::Below(Alignment::Right, rel) => Rect::new(
444                rel.x + right(area.width, rel.width),
445                rel.bottom(),
446                area.width,
447                area.height,
448            ),
449
450            PopupConstraint::Left(Alignment::Left, rel) => Rect::new(
451                rel.x.saturating_sub(area.width),
452                rel.y,
453                area.width,
454                area.height,
455            ),
456            PopupConstraint::Left(Alignment::Center, rel) => Rect::new(
457                rel.x.saturating_sub(area.width),
458                rel.y + middle(area.height, rel.height),
459                area.width,
460                area.height,
461            ),
462            PopupConstraint::Left(Alignment::Right, rel) => Rect::new(
463                rel.x.saturating_sub(area.width),
464                rel.y + bottom(area.height, rel.height),
465                area.width,
466                area.height,
467            ),
468            PopupConstraint::Right(Alignment::Left, rel) => Rect::new(
469                rel.right(), //
470                rel.y,
471                area.width,
472                area.height,
473            ),
474            PopupConstraint::Right(Alignment::Center, rel) => Rect::new(
475                rel.right(),
476                rel.y + middle(area.height, rel.height),
477                area.width,
478                area.height,
479            ),
480            PopupConstraint::Right(Alignment::Right, rel) => Rect::new(
481                rel.right(),
482                rel.y + bottom(area.height, rel.height),
483                area.width,
484                area.height,
485            ),
486
487            PopupConstraint::Position(x, y) => Rect::new(
488                x, //
489                y,
490                area.width,
491                area.height,
492            ),
493
494            PopupConstraint::AboveOrBelow(Alignment::Left, rel) => {
495                if area.height.saturating_add_signed(-self.offset.1) < rel.y {
496                    Rect::new(
497                        rel.x,
498                        rel.y.saturating_sub(area.height),
499                        area.width,
500                        area.height,
501                    )
502                } else {
503                    offset = (offset.0, -offset.1);
504                    Rect::new(
505                        rel.x, //
506                        rel.bottom(),
507                        area.width,
508                        area.height,
509                    )
510                }
511            }
512            PopupConstraint::AboveOrBelow(Alignment::Center, rel) => {
513                if area.height.saturating_add_signed(-self.offset.1) < rel.y {
514                    Rect::new(
515                        rel.x + center(area.width, rel.width),
516                        rel.y.saturating_sub(area.height),
517                        area.width,
518                        area.height,
519                    )
520                } else {
521                    offset = (offset.0, -offset.1);
522                    Rect::new(
523                        rel.x + center(area.width, rel.width), //
524                        rel.bottom(),
525                        area.width,
526                        area.height,
527                    )
528                }
529            }
530            PopupConstraint::AboveOrBelow(Alignment::Right, rel) => {
531                if area.height.saturating_add_signed(-self.offset.1) < rel.y {
532                    Rect::new(
533                        rel.x + right(area.width, rel.width),
534                        rel.y.saturating_sub(area.height),
535                        area.width,
536                        area.height,
537                    )
538                } else {
539                    offset = (offset.0, -offset.1);
540                    Rect::new(
541                        rel.x + right(area.width, rel.width), //
542                        rel.bottom(),
543                        area.width,
544                        area.height,
545                    )
546                }
547            }
548            PopupConstraint::BelowOrAbove(Alignment::Left, rel) => {
549                if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
550                    <= boundary_area.height
551                {
552                    Rect::new(
553                        rel.x, //
554                        rel.bottom(),
555                        area.width,
556                        area.height,
557                    )
558                } else {
559                    offset = (offset.0, -offset.1);
560                    Rect::new(
561                        rel.x,
562                        rel.y.saturating_sub(area.height),
563                        area.width,
564                        area.height,
565                    )
566                }
567            }
568            PopupConstraint::BelowOrAbove(Alignment::Center, rel) => {
569                if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
570                    <= boundary_area.height
571                {
572                    Rect::new(
573                        rel.x + center(area.width, rel.width), //
574                        rel.bottom(),
575                        area.width,
576                        area.height,
577                    )
578                } else {
579                    offset = (offset.0, -offset.1);
580                    Rect::new(
581                        rel.x + center(area.width, rel.width),
582                        rel.y.saturating_sub(area.height),
583                        area.width,
584                        area.height,
585                    )
586                }
587            }
588            PopupConstraint::BelowOrAbove(Alignment::Right, rel) => {
589                if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
590                    <= boundary_area.height
591                {
592                    Rect::new(
593                        rel.x + right(area.width, rel.width), //
594                        rel.bottom(),
595                        area.width,
596                        area.height,
597                    )
598                } else {
599                    offset = (offset.0, -offset.1);
600                    Rect::new(
601                        rel.x + right(area.width, rel.width),
602                        rel.y.saturating_sub(area.height),
603                        area.width,
604                        area.height,
605                    )
606                }
607            }
608        };
609
610        // offset
611        area.x = area.x.saturating_add_signed(offset.0);
612        area.y = area.y.saturating_add_signed(offset.1);
613
614        // keep in sight
615        if area.left() < boundary_area.left() {
616            area.x = boundary_area.left();
617        }
618        if area.right() >= boundary_area.right() {
619            let corr = area.right().saturating_sub(boundary_area.right());
620            area.x = max(boundary_area.left(), area.x.saturating_sub(corr));
621        }
622        if area.top() < boundary_area.top() {
623            area.y = boundary_area.top();
624        }
625        if area.bottom() >= boundary_area.bottom() {
626            let corr = area.bottom().saturating_sub(boundary_area.bottom());
627            area.y = max(boundary_area.top(), area.y.saturating_sub(corr));
628        }
629
630        // shrink to size
631        if area.right() > boundary_area.right() {
632            let corr = area.right() - boundary_area.right();
633            area.width = area.width.saturating_sub(corr);
634        }
635        if area.bottom() > boundary_area.bottom() {
636            let corr = area.bottom() - boundary_area.bottom();
637            area.height = area.height.saturating_sub(corr);
638        }
639
640        area
641    }
642}
643
644impl Default for PopupStyle {
645    #[allow(deprecated)]
646    fn default() -> Self {
647        Self {
648            style: Default::default(),
649            offset: None,
650            block: None,
651            border_style: None,
652            scroll: None,
653            alignment: None,
654            placement: None,
655            non_exhaustive: NonExhaustive,
656        }
657    }
658}
659
660impl Clone for PopupCoreState {
661    #[allow(deprecated)]
662    fn clone(&self) -> Self {
663        Self {
664            area: self.area,
665            area_z: self.area_z,
666            widget_area: self.widget_area,
667            h_scroll: self.h_scroll.clone(),
668            v_scroll: self.v_scroll.clone(),
669            active: self.active.clone(),
670            mouse: Default::default(),
671            non_exhaustive: NonExhaustive,
672        }
673    }
674}
675
676impl Default for PopupCoreState {
677    #[allow(deprecated)]
678    fn default() -> Self {
679        Self {
680            area: Default::default(),
681            area_z: 1,
682            widget_area: Default::default(),
683            h_scroll: Default::default(),
684            v_scroll: Default::default(),
685            active: Default::default(),
686            mouse: Default::default(),
687            non_exhaustive: NonExhaustive,
688        }
689    }
690}
691
692impl RelocatableState for PopupCoreState {
693    #[allow(deprecated)]
694    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
695        self.area = relocate_area(self.area, shift, clip);
696        self.widget_area = relocate_area(self.widget_area, shift, clip);
697    }
698}
699
700impl PopupCoreState {
701    /// New
702    #[inline]
703    pub fn new() -> Self {
704        Default::default()
705    }
706
707    /// New with a focus name.
708    #[deprecated(since = "1.0.2", note = "name is ignored")]
709    pub fn named(_name: &str) -> Self {
710        Default::default()
711    }
712
713    /// Set the z-index of the popup.
714    #[deprecated(since = "1.2.0", note = "job for the main widget")]
715    pub fn set_area_z(&mut self, z: u16) {
716        self.area_z = z;
717    }
718
719    /// The z-index of the popup.
720    #[deprecated(since = "1.2.0", note = "job for the main widget")]
721    pub fn area_z(&self) -> u16 {
722        self.area_z
723    }
724
725    /// Is the popup active/visible.
726    #[allow(deprecated)]
727    pub fn is_active(&self) -> bool {
728        self.active.get()
729    }
730
731    /// Flip visibility of the popup.
732    pub fn flip_active(&mut self) {
733        self.set_active(!self.is_active());
734    }
735
736    /// Show the popup.
737    /// This will set gained/lost flags according to the change.
738    /// If the popup is hidden this will clear all flags.
739    #[allow(deprecated)]
740    pub fn set_active(&mut self, active: bool) -> bool {
741        let old_value = self.is_active();
742        self.active.set(active);
743        old_value != self.is_active()
744    }
745
746    /// Clear all stored areas.
747    #[allow(deprecated)]
748    pub fn clear_areas(&mut self) {
749        self.area = Default::default();
750        self.widget_area = Default::default();
751        self.v_scroll.area = Default::default();
752        self.h_scroll.area = Default::default();
753    }
754}
755
756impl HandleEvent<crossterm::event::Event, Popup, PopupOutcome> for PopupCoreState {
757    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> PopupOutcome {
758        if self.is_active() {
759            match event {
760                ct_event!(mouse down Left for x,y)
761                | ct_event!(mouse down Right for x,y)
762                | ct_event!(mouse down Middle for x,y)
763                    if !self.area.contains((*x, *y).into()) =>
764                {
765                    PopupOutcome::Hide
766                }
767                _ => PopupOutcome::Continue,
768            }
769        } else {
770            PopupOutcome::Continue
771        }
772    }
773}