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#[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 pub constraint: Cell<PopupConstraint>,
44 pub offset: (i16, i16),
47 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#[derive(Debug, Clone)]
63pub struct PopupStyle {
64 #[deprecated(since = "1.2.0", note = "job for the main widget")]
66 pub style: Style,
67 pub offset: Option<(i16, i16)>,
69 #[deprecated(since = "1.2.0", note = "job for the main widget")]
71 pub block: Option<Block<'static>>,
72 #[deprecated(since = "1.2.0", note = "job for the main widget")]
74 pub border_style: Option<Style>,
75 #[deprecated(since = "1.2.0", note = "job for the main widget")]
77 pub scroll: Option<ScrollStyle>,
78 pub alignment: Option<Alignment>,
80 pub placement: Option<Placement>,
82
83 pub non_exhaustive: NonExhaustive,
85}
86
87#[derive(Debug)]
89pub struct PopupCoreState {
90 pub area: Rect,
95 pub area_z: u16,
97 #[deprecated(since = "1.2.0", note = "use area instead")]
100 pub widget_area: Rect,
101
102 #[deprecated(since = "1.2.0", note = "job for the main widget")]
105 pub h_scroll: ScrollState,
106 #[deprecated(since = "1.2.0", note = "job for the main widget")]
109 pub v_scroll: ScrollState,
110
111 #[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 pub mouse: MouseFlags,
128
129 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 pub fn new() -> Self {
152 Self::default()
153 }
154
155 pub fn ref_constraint(&self, constraint: PopupConstraint) -> &Self {
157 self.constraint.set(constraint);
158 self
159 }
160
161 pub fn constraint(self, constraint: PopupConstraint) -> Self {
163 self.constraint.set(constraint);
164 self
165 }
166
167 pub fn offset(mut self, offset: (i16, i16)) -> Self {
174 self.offset = offset;
175 self
176 }
177
178 pub fn x_offset(mut self, offset: i16) -> Self {
181 self.offset.0 = offset;
182 self
183 }
184
185 pub fn y_offset(mut self, offset: i16) -> Self {
188 self.offset.1 = offset;
189 self
190 }
191
192 pub fn boundary(mut self, boundary: Rect) -> Self {
200 self.boundary_area = Some(boundary);
201 self
202 }
203
204 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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
376pub 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
385pub 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 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 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, 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(), 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, 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, 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), 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), 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, 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), 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), 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 area.x = area.x.saturating_add_signed(offset.0);
612 area.y = area.y.saturating_add_signed(offset.1);
613
614 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 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 #[inline]
703 pub fn new() -> Self {
704 Default::default()
705 }
706
707 #[deprecated(since = "1.0.2", note = "name is ignored")]
709 pub fn named(_name: &str) -> Self {
710 Default::default()
711 }
712
713 #[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 #[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 #[allow(deprecated)]
727 pub fn is_active(&self) -> bool {
728 self.active.get()
729 }
730
731 pub fn flip_active(&mut self) {
733 self.set_active(!self.is_active());
734 }
735
736 #[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 #[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}