1use crate::_private::NonExhaustive;
2use crate::event::PopupOutcome;
3use crate::{Placement, PopupConstraint};
4use rat_event::util::MouseFlags;
5use rat_event::{ct_event, HandleEvent, Popup};
6use rat_focus::FocusFlag;
7use rat_reloc::{relocate_area, RelocatableState};
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 pub style: Style,
40
41 pub constraint: Cell<PopupConstraint>,
42 pub offset: (i16, i16),
43 pub boundary_area: Option<Rect>,
44
45 pub block: Option<Block<'a>>,
46 pub h_scroll: Option<Scroll<'a>>,
47 pub v_scroll: Option<Scroll<'a>>,
48
49 pub non_exhaustive: NonExhaustive,
50}
51
52#[derive(Debug, Clone)]
54pub struct PopupStyle {
55 pub style: Style,
57 pub offset: Option<(i16, i16)>,
59 pub block: Option<Block<'static>>,
61 pub border_style: Option<Style>,
63 pub scroll: Option<ScrollStyle>,
65 pub alignment: Option<Alignment>,
67 pub placement: Option<Placement>,
69
70 pub non_exhaustive: NonExhaustive,
72}
73
74#[derive(Debug)]
76pub struct PopupCoreState {
77 pub area: Rect,
82 pub area_z: u16,
84 pub widget_area: Rect,
87
88 pub h_scroll: ScrollState,
91 pub v_scroll: ScrollState,
94
95 #[deprecated(
108 since = "1.0.2",
109 note = "use is_active() and set_active() instead. will change type."
110 )]
111 pub active: FocusFlag,
112
113 pub mouse: MouseFlags,
116
117 pub non_exhaustive: NonExhaustive,
119}
120
121impl Default for PopupCore<'_> {
122 fn default() -> Self {
123 Self {
124 style: Default::default(),
125 constraint: Cell::new(PopupConstraint::None),
126 offset: (0, 0),
127 boundary_area: None,
128 block: None,
129 h_scroll: None,
130 v_scroll: None,
131 non_exhaustive: NonExhaustive,
132 }
133 }
134}
135
136impl<'a> PopupCore<'a> {
137 pub fn new() -> Self {
139 Self::default()
140 }
141
142 pub fn ref_constraint(&self, constraint: PopupConstraint) -> &Self {
144 self.constraint.set(constraint);
145 self
146 }
147
148 pub fn constraint(self, constraint: PopupConstraint) -> Self {
150 self.constraint.set(constraint);
151 self
152 }
153
154 pub fn offset(mut self, offset: (i16, i16)) -> Self {
161 self.offset = offset;
162 self
163 }
164
165 pub fn x_offset(mut self, offset: i16) -> Self {
168 self.offset.0 = offset;
169 self
170 }
171
172 pub fn y_offset(mut self, offset: i16) -> Self {
175 self.offset.1 = offset;
176 self
177 }
178
179 pub fn boundary(mut self, boundary: Rect) -> Self {
186 self.boundary_area = Some(boundary);
187 self
188 }
189
190 pub fn styles(mut self, styles: PopupStyle) -> Self {
192 self.style = styles.style;
193 if let Some(offset) = styles.offset {
194 self.offset = offset;
195 }
196 self.block = self.block.map(|v| v.style(self.style));
197 if let Some(border_style) = styles.border_style {
198 self.block = self.block.map(|v| v.border_style(border_style));
199 }
200 if let Some(block) = styles.block {
201 self.block = Some(block);
202 }
203 if let Some(styles) = styles.scroll {
204 if let Some(h_scroll) = self.h_scroll {
205 self.h_scroll = Some(h_scroll.styles(styles.clone()));
206 }
207 if let Some(v_scroll) = self.v_scroll {
208 self.v_scroll = Some(v_scroll.styles(styles));
209 }
210 }
211
212 self
213 }
214
215 pub fn style(mut self, style: Style) -> Self {
217 self.style = style;
218 self.block = self.block.map(|v| v.style(self.style));
219 self
220 }
221
222 pub fn block(mut self, block: Block<'a>) -> Self {
224 self.block = Some(block);
225 self.block = self.block.map(|v| v.style(self.style));
226 self
227 }
228
229 pub fn block_opt(mut self, block: Option<Block<'a>>) -> Self {
231 self.block = block;
232 self.block = self.block.map(|v| v.style(self.style));
233 self
234 }
235
236 pub fn h_scroll(mut self, h_scroll: Scroll<'a>) -> Self {
238 self.h_scroll = Some(h_scroll);
239 self
240 }
241
242 pub fn h_scroll_opt(mut self, h_scroll: Option<Scroll<'a>>) -> Self {
244 self.h_scroll = h_scroll;
245 self
246 }
247
248 pub fn v_scroll(mut self, v_scroll: Scroll<'a>) -> Self {
250 self.v_scroll = Some(v_scroll);
251 self
252 }
253
254 pub fn v_scroll_opt(mut self, v_scroll: Option<Scroll<'a>>) -> Self {
256 self.v_scroll = v_scroll;
257 self
258 }
259
260 pub fn get_block_size(&self) -> Size {
262 let area = Rect::new(0, 0, 20, 20);
263 let inner = self.block.inner_if_some(area);
264 Size {
265 width: (inner.left() - area.left()) + (area.right() - inner.right()),
266 height: (inner.top() - area.top()) + (area.bottom() - inner.bottom()),
267 }
268 }
269
270 pub fn get_block_padding(&self) -> Padding {
272 let area = Rect::new(0, 0, 20, 20);
273 let inner = self.block.inner_if_some(area);
274 Padding {
275 left: inner.left() - area.left(),
276 right: area.right() - inner.right(),
277 top: inner.top() - area.top(),
278 bottom: area.bottom() - inner.bottom(),
279 }
280 }
281
282 pub fn inner(&self, area: Rect) -> Rect {
284 self.block.inner_if_some(area)
285 }
286
287 pub fn layout(&self, area: Rect, buf: &Buffer) -> Rect {
289 self._layout(area, self.boundary_area.unwrap_or(buf.area))
290 }
291}
292
293impl<'a> StatefulWidget for &'a PopupCore<'a> {
294 type State = PopupCoreState;
295
296 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
297 render_popup(self, area, buf, state);
298 }
299}
300
301impl StatefulWidget for PopupCore<'_> {
302 type State = PopupCoreState;
303
304 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
305 render_popup(&self, area, buf, state);
306 }
307}
308
309#[allow(deprecated)]
310fn render_popup(widget: &PopupCore<'_>, area: Rect, buf: &mut Buffer, state: &mut PopupCoreState) {
311 if !state.active.get() {
312 state.clear_areas();
313 return;
314 }
315
316 state.area = widget._layout(area, widget.boundary_area.unwrap_or(buf.area));
317
318 reset_buf_area(state.area, buf);
319
320 let sa = ScrollArea::new()
321 .block(widget.block.as_ref())
322 .h_scroll(widget.h_scroll.as_ref())
323 .v_scroll(widget.v_scroll.as_ref())
324 .style(fallback_popup_style(widget.style));
325
326 state.widget_area = sa.inner(state.area, Some(&state.h_scroll), Some(&state.v_scroll));
327
328 sa.render(
329 state.area,
330 buf,
331 &mut ScrollAreaState::new()
332 .h_scroll(&mut state.h_scroll)
333 .v_scroll(&mut state.v_scroll),
334 );
335}
336
337pub fn fallback_popup_style(style: Style) -> Style {
339 if style.fg.is_some() || style.bg.is_some() {
340 style
341 } else {
342 style.black().on_gray()
343 }
344}
345
346pub fn reset_buf_area(area: Rect, buf: &mut Buffer) {
348 for y in area.top()..area.bottom() {
349 for x in area.left()..area.right() {
350 if let Some(cell) = buf.cell_mut((x, y)) {
351 cell.reset();
352 }
353 }
354 }
355}
356
357impl PopupCore<'_> {
358 fn _layout(&self, area: Rect, boundary_area: Rect) -> Rect {
359 fn center(len: u16, within: u16) -> u16 {
361 ((within as i32 - len as i32) / 2).clamp(0, i16::MAX as i32) as u16
362 }
363 let middle = center;
364 fn right(len: u16, within: u16) -> u16 {
365 within.saturating_sub(len)
366 }
367 let bottom = right;
368
369 let mut offset = self.offset;
371
372 let mut area = match self.constraint.get() {
373 PopupConstraint::None => area,
374 PopupConstraint::Above(Alignment::Left, rel) => Rect::new(
375 rel.x,
376 rel.y.saturating_sub(area.height),
377 area.width,
378 area.height,
379 ),
380 PopupConstraint::Above(Alignment::Center, rel) => Rect::new(
381 rel.x + center(area.width, rel.width),
382 rel.y.saturating_sub(area.height),
383 area.width,
384 area.height,
385 ),
386 PopupConstraint::Above(Alignment::Right, rel) => Rect::new(
387 rel.x + right(area.width, rel.width),
388 rel.y.saturating_sub(area.height),
389 area.width,
390 area.height,
391 ),
392 PopupConstraint::Below(Alignment::Left, rel) => Rect::new(
393 rel.x, rel.bottom(),
395 area.width,
396 area.height,
397 ),
398 PopupConstraint::Below(Alignment::Center, rel) => Rect::new(
399 rel.x + center(area.width, rel.width),
400 rel.bottom(),
401 area.width,
402 area.height,
403 ),
404 PopupConstraint::Below(Alignment::Right, rel) => Rect::new(
405 rel.x + right(area.width, rel.width),
406 rel.bottom(),
407 area.width,
408 area.height,
409 ),
410
411 PopupConstraint::Left(Alignment::Left, rel) => Rect::new(
412 rel.x.saturating_sub(area.width),
413 rel.y,
414 area.width,
415 area.height,
416 ),
417 PopupConstraint::Left(Alignment::Center, rel) => Rect::new(
418 rel.x.saturating_sub(area.width),
419 rel.y + middle(area.height, rel.height),
420 area.width,
421 area.height,
422 ),
423 PopupConstraint::Left(Alignment::Right, rel) => Rect::new(
424 rel.x.saturating_sub(area.width),
425 rel.y + bottom(area.height, rel.height),
426 area.width,
427 area.height,
428 ),
429 PopupConstraint::Right(Alignment::Left, rel) => Rect::new(
430 rel.right(), rel.y,
432 area.width,
433 area.height,
434 ),
435 PopupConstraint::Right(Alignment::Center, rel) => Rect::new(
436 rel.right(),
437 rel.y + middle(area.height, rel.height),
438 area.width,
439 area.height,
440 ),
441 PopupConstraint::Right(Alignment::Right, rel) => Rect::new(
442 rel.right(),
443 rel.y + bottom(area.height, rel.height),
444 area.width,
445 area.height,
446 ),
447
448 PopupConstraint::Position(x, y) => Rect::new(
449 x, y,
451 area.width,
452 area.height,
453 ),
454
455 PopupConstraint::AboveOrBelow(Alignment::Left, rel) => {
456 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
457 Rect::new(
458 rel.x,
459 rel.y.saturating_sub(area.height),
460 area.width,
461 area.height,
462 )
463 } else {
464 offset = (offset.0, -offset.1);
465 Rect::new(
466 rel.x, rel.bottom(),
468 area.width,
469 area.height,
470 )
471 }
472 }
473 PopupConstraint::AboveOrBelow(Alignment::Center, rel) => {
474 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
475 Rect::new(
476 rel.x + center(area.width, rel.width),
477 rel.y.saturating_sub(area.height),
478 area.width,
479 area.height,
480 )
481 } else {
482 offset = (offset.0, -offset.1);
483 Rect::new(
484 rel.x + center(area.width, rel.width), rel.bottom(),
486 area.width,
487 area.height,
488 )
489 }
490 }
491 PopupConstraint::AboveOrBelow(Alignment::Right, rel) => {
492 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
493 Rect::new(
494 rel.x + right(area.width, rel.width),
495 rel.y.saturating_sub(area.height),
496 area.width,
497 area.height,
498 )
499 } else {
500 offset = (offset.0, -offset.1);
501 Rect::new(
502 rel.x + right(area.width, rel.width), rel.bottom(),
504 area.width,
505 area.height,
506 )
507 }
508 }
509 PopupConstraint::BelowOrAbove(Alignment::Left, rel) => {
510 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
511 <= boundary_area.height
512 {
513 Rect::new(
514 rel.x, rel.bottom(),
516 area.width,
517 area.height,
518 )
519 } else {
520 offset = (offset.0, -offset.1);
521 Rect::new(
522 rel.x,
523 rel.y.saturating_sub(area.height),
524 area.width,
525 area.height,
526 )
527 }
528 }
529 PopupConstraint::BelowOrAbove(Alignment::Center, rel) => {
530 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
531 <= boundary_area.height
532 {
533 Rect::new(
534 rel.x + center(area.width, rel.width), rel.bottom(),
536 area.width,
537 area.height,
538 )
539 } else {
540 offset = (offset.0, -offset.1);
541 Rect::new(
542 rel.x + center(area.width, rel.width),
543 rel.y.saturating_sub(area.height),
544 area.width,
545 area.height,
546 )
547 }
548 }
549 PopupConstraint::BelowOrAbove(Alignment::Right, rel) => {
550 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
551 <= boundary_area.height
552 {
553 Rect::new(
554 rel.x + right(area.width, rel.width), rel.bottom(),
556 area.width,
557 area.height,
558 )
559 } else {
560 offset = (offset.0, -offset.1);
561 Rect::new(
562 rel.x + right(area.width, rel.width),
563 rel.y.saturating_sub(area.height),
564 area.width,
565 area.height,
566 )
567 }
568 }
569 };
570
571 area.x = area.x.saturating_add_signed(offset.0);
573 area.y = area.y.saturating_add_signed(offset.1);
574
575 if area.left() < boundary_area.left() {
577 area.x = boundary_area.left();
578 }
579 if area.right() >= boundary_area.right() {
580 let corr = area.right().saturating_sub(boundary_area.right());
581 area.x = max(boundary_area.left(), area.x.saturating_sub(corr));
582 }
583 if area.top() < boundary_area.top() {
584 area.y = boundary_area.top();
585 }
586 if area.bottom() >= boundary_area.bottom() {
587 let corr = area.bottom().saturating_sub(boundary_area.bottom());
588 area.y = max(boundary_area.top(), area.y.saturating_sub(corr));
589 }
590
591 if area.right() > boundary_area.right() {
593 let corr = area.right() - boundary_area.right();
594 area.width = area.width.saturating_sub(corr);
595 }
596 if area.bottom() > boundary_area.bottom() {
597 let corr = area.bottom() - boundary_area.bottom();
598 area.height = area.height.saturating_sub(corr);
599 }
600
601 area
602 }
603}
604
605impl Default for PopupStyle {
606 fn default() -> Self {
607 Self {
608 style: Default::default(),
609 offset: None,
610 block: None,
611 border_style: None,
612 scroll: None,
613 alignment: None,
614 placement: None,
615 non_exhaustive: NonExhaustive,
616 }
617 }
618}
619
620impl Clone for PopupCoreState {
621 #[allow(deprecated)]
622 fn clone(&self) -> Self {
623 Self {
624 area: self.area,
625 area_z: self.area_z,
626 widget_area: self.widget_area,
627 h_scroll: self.h_scroll.clone(),
628 v_scroll: self.v_scroll.clone(),
629 active: self.active.clone(),
630 mouse: Default::default(),
631 non_exhaustive: NonExhaustive,
632 }
633 }
634}
635
636impl Default for PopupCoreState {
637 #[allow(deprecated)]
638 fn default() -> Self {
639 Self {
640 area: Default::default(),
641 area_z: 1,
642 widget_area: Default::default(),
643 h_scroll: Default::default(),
644 v_scroll: Default::default(),
645 active: Default::default(),
646 mouse: Default::default(),
647 non_exhaustive: NonExhaustive,
648 }
649 }
650}
651
652impl RelocatableState for PopupCoreState {
653 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
654 self.area = relocate_area(self.area, shift, clip);
655 self.widget_area = relocate_area(self.widget_area, shift, clip);
656 }
657}
658
659impl PopupCoreState {
660 #[inline]
662 pub fn new() -> Self {
663 Default::default()
664 }
665
666 #[deprecated(since = "1.0.2", note = "name is ignored")]
668 pub fn named(_name: &str) -> Self {
669 Default::default()
670 }
671
672 pub fn set_area_z(&mut self, z: u16) {
674 self.area_z = z;
675 }
676
677 pub fn area_z(&self) -> u16 {
679 self.area_z
680 }
681
682 #[allow(deprecated)]
684 pub fn is_active(&self) -> bool {
685 self.active.get()
686 }
687
688 pub fn flip_active(&mut self) {
690 self.set_active(!self.is_active());
691 }
692
693 #[allow(deprecated)]
697 pub fn set_active(&mut self, active: bool) -> bool {
698 let old_value = self.is_active();
699 self.active.set(active);
700 old_value != self.is_active()
701 }
702
703 pub fn clear_areas(&mut self) {
705 self.area = Default::default();
706 self.widget_area = Default::default();
707 self.v_scroll.area = Default::default();
708 self.h_scroll.area = Default::default();
709 }
710}
711
712impl HandleEvent<crossterm::event::Event, Popup, PopupOutcome> for PopupCoreState {
713 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> PopupOutcome {
714 if self.is_active() {
715 match event {
716 ct_event!(mouse down Left for x,y)
717 | ct_event!(mouse down Right for x,y)
718 | ct_event!(mouse down Middle for x,y)
719 if !self.area.contains((*x, *y).into()) =>
720 {
721 PopupOutcome::Hide
722 }
723 _ => PopupOutcome::Continue,
724 }
725 } else {
726 PopupOutcome::Continue
727 }
728 }
729}