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, HasFocus};
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 pub active: FocusFlag,
108
109 pub mouse: MouseFlags,
112
113 pub non_exhaustive: NonExhaustive,
115}
116
117impl Default for PopupCore<'_> {
118 fn default() -> Self {
119 Self {
120 style: Default::default(),
121 constraint: Cell::new(PopupConstraint::None),
122 offset: (0, 0),
123 boundary_area: None,
124 block: None,
125 h_scroll: None,
126 v_scroll: None,
127 non_exhaustive: NonExhaustive,
128 }
129 }
130}
131
132impl<'a> PopupCore<'a> {
133 pub fn new() -> Self {
135 Self::default()
136 }
137
138 pub fn ref_constraint(&self, constraint: PopupConstraint) -> &Self {
140 self.constraint.set(constraint);
141 self
142 }
143
144 pub fn constraint(self, constraint: PopupConstraint) -> Self {
146 self.constraint.set(constraint);
147 self
148 }
149
150 pub fn offset(mut self, offset: (i16, i16)) -> Self {
157 self.offset = offset;
158 self
159 }
160
161 pub fn x_offset(mut self, offset: i16) -> Self {
164 self.offset.0 = offset;
165 self
166 }
167
168 pub fn y_offset(mut self, offset: i16) -> Self {
171 self.offset.1 = offset;
172 self
173 }
174
175 pub fn boundary(mut self, boundary: Rect) -> Self {
182 self.boundary_area = Some(boundary);
183 self
184 }
185
186 pub fn styles(mut self, styles: PopupStyle) -> Self {
188 self.style = styles.style;
189 if let Some(offset) = styles.offset {
190 self.offset = offset;
191 }
192 self.block = self.block.map(|v| v.style(self.style));
193 if let Some(border_style) = styles.border_style {
194 self.block = self.block.map(|v| v.border_style(border_style));
195 }
196 if let Some(block) = styles.block {
197 self.block = Some(block);
198 }
199 if let Some(styles) = styles.scroll {
200 if let Some(h_scroll) = self.h_scroll {
201 self.h_scroll = Some(h_scroll.styles(styles.clone()));
202 }
203 if let Some(v_scroll) = self.v_scroll {
204 self.v_scroll = Some(v_scroll.styles(styles));
205 }
206 }
207
208 self
209 }
210
211 pub fn style(mut self, style: Style) -> Self {
213 self.style = style;
214 self.block = self.block.map(|v| v.style(self.style));
215 self
216 }
217
218 pub fn block(mut self, block: Block<'a>) -> Self {
220 self.block = Some(block);
221 self.block = self.block.map(|v| v.style(self.style));
222 self
223 }
224
225 pub fn block_opt(mut self, block: Option<Block<'a>>) -> Self {
227 self.block = block;
228 self.block = self.block.map(|v| v.style(self.style));
229 self
230 }
231
232 pub fn h_scroll(mut self, h_scroll: Scroll<'a>) -> Self {
234 self.h_scroll = Some(h_scroll);
235 self
236 }
237
238 pub fn h_scroll_opt(mut self, h_scroll: Option<Scroll<'a>>) -> Self {
240 self.h_scroll = h_scroll;
241 self
242 }
243
244 pub fn v_scroll(mut self, v_scroll: Scroll<'a>) -> Self {
246 self.v_scroll = Some(v_scroll);
247 self
248 }
249
250 pub fn v_scroll_opt(mut self, v_scroll: Option<Scroll<'a>>) -> Self {
252 self.v_scroll = v_scroll;
253 self
254 }
255
256 pub fn get_block_size(&self) -> Size {
258 let area = Rect::new(0, 0, 20, 20);
259 let inner = self.block.inner_if_some(area);
260 Size {
261 width: (inner.left() - area.left()) + (area.right() - inner.right()),
262 height: (inner.top() - area.top()) + (area.bottom() - inner.bottom()),
263 }
264 }
265
266 pub fn get_block_padding(&self) -> Padding {
268 let area = Rect::new(0, 0, 20, 20);
269 let inner = self.block.inner_if_some(area);
270 Padding {
271 left: inner.left() - area.left(),
272 right: area.right() - inner.right(),
273 top: inner.top() - area.top(),
274 bottom: area.bottom() - inner.bottom(),
275 }
276 }
277
278 pub fn inner(&self, area: Rect) -> Rect {
280 self.block.inner_if_some(area)
281 }
282
283 pub fn layout(&self, area: Rect, buf: &Buffer) -> Rect {
285 self._layout(area, self.boundary_area.unwrap_or(buf.area))
286 }
287}
288
289impl<'a> StatefulWidget for &'a PopupCore<'a> {
290 type State = PopupCoreState;
291
292 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
293 render_popup(self, area, buf, state);
294 }
295}
296
297impl StatefulWidget for PopupCore<'_> {
298 type State = PopupCoreState;
299
300 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
301 render_popup(&self, area, buf, state);
302 }
303}
304
305fn render_popup(widget: &PopupCore<'_>, area: Rect, buf: &mut Buffer, state: &mut PopupCoreState) {
306 if !state.active.is_focused() {
307 state.clear_areas();
308 return;
309 }
310
311 state.area = widget._layout(area, widget.boundary_area.unwrap_or(buf.area));
312
313 reset_buf_area(state.area, buf);
314
315 let sa = ScrollArea::new()
316 .block(widget.block.as_ref())
317 .h_scroll(widget.h_scroll.as_ref())
318 .v_scroll(widget.v_scroll.as_ref())
319 .style(fallback_popup_style(widget.style));
320
321 state.widget_area = sa.inner(state.area, Some(&state.h_scroll), Some(&state.v_scroll));
322
323 sa.render(
324 state.area,
325 buf,
326 &mut ScrollAreaState::new()
327 .h_scroll(&mut state.h_scroll)
328 .v_scroll(&mut state.v_scroll),
329 );
330}
331
332pub fn fallback_popup_style(style: Style) -> Style {
334 if style.fg.is_some() || style.bg.is_some() {
335 style
336 } else {
337 style.black().on_gray()
338 }
339}
340
341pub fn reset_buf_area(area: Rect, buf: &mut Buffer) {
343 for y in area.top()..area.bottom() {
344 for x in area.left()..area.right() {
345 if let Some(cell) = buf.cell_mut((x, y)) {
346 cell.reset();
347 }
348 }
349 }
350}
351
352impl PopupCore<'_> {
353 fn _layout(&self, area: Rect, boundary_area: Rect) -> Rect {
354 fn center(len: u16, within: u16) -> u16 {
356 ((within as i32 - len as i32) / 2).clamp(0, i16::MAX as i32) as u16
357 }
358 let middle = center;
359 fn right(len: u16, within: u16) -> u16 {
360 within.saturating_sub(len)
361 }
362 let bottom = right;
363
364 let mut offset = self.offset;
366
367 let mut area = match self.constraint.get() {
368 PopupConstraint::None => area,
369 PopupConstraint::Above(Alignment::Left, rel) => Rect::new(
370 rel.x,
371 rel.y.saturating_sub(area.height),
372 area.width,
373 area.height,
374 ),
375 PopupConstraint::Above(Alignment::Center, rel) => Rect::new(
376 rel.x + center(area.width, rel.width),
377 rel.y.saturating_sub(area.height),
378 area.width,
379 area.height,
380 ),
381 PopupConstraint::Above(Alignment::Right, rel) => Rect::new(
382 rel.x + right(area.width, rel.width),
383 rel.y.saturating_sub(area.height),
384 area.width,
385 area.height,
386 ),
387 PopupConstraint::Below(Alignment::Left, rel) => Rect::new(
388 rel.x, rel.bottom(),
390 area.width,
391 area.height,
392 ),
393 PopupConstraint::Below(Alignment::Center, rel) => Rect::new(
394 rel.x + center(area.width, rel.width),
395 rel.bottom(),
396 area.width,
397 area.height,
398 ),
399 PopupConstraint::Below(Alignment::Right, rel) => Rect::new(
400 rel.x + right(area.width, rel.width),
401 rel.bottom(),
402 area.width,
403 area.height,
404 ),
405
406 PopupConstraint::Left(Alignment::Left, rel) => Rect::new(
407 rel.x.saturating_sub(area.width),
408 rel.y,
409 area.width,
410 area.height,
411 ),
412 PopupConstraint::Left(Alignment::Center, rel) => Rect::new(
413 rel.x.saturating_sub(area.width),
414 rel.y + middle(area.height, rel.height),
415 area.width,
416 area.height,
417 ),
418 PopupConstraint::Left(Alignment::Right, rel) => Rect::new(
419 rel.x.saturating_sub(area.width),
420 rel.y + bottom(area.height, rel.height),
421 area.width,
422 area.height,
423 ),
424 PopupConstraint::Right(Alignment::Left, rel) => Rect::new(
425 rel.right(), rel.y,
427 area.width,
428 area.height,
429 ),
430 PopupConstraint::Right(Alignment::Center, rel) => Rect::new(
431 rel.right(),
432 rel.y + middle(area.height, rel.height),
433 area.width,
434 area.height,
435 ),
436 PopupConstraint::Right(Alignment::Right, rel) => Rect::new(
437 rel.right(),
438 rel.y + bottom(area.height, rel.height),
439 area.width,
440 area.height,
441 ),
442
443 PopupConstraint::Position(x, y) => Rect::new(
444 x, y,
446 area.width,
447 area.height,
448 ),
449
450 PopupConstraint::AboveOrBelow(Alignment::Left, rel) => {
451 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
452 Rect::new(
453 rel.x,
454 rel.y.saturating_sub(area.height),
455 area.width,
456 area.height,
457 )
458 } else {
459 offset = (offset.0, -offset.1);
460 Rect::new(
461 rel.x, rel.bottom(),
463 area.width,
464 area.height,
465 )
466 }
467 }
468 PopupConstraint::AboveOrBelow(Alignment::Center, rel) => {
469 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
470 Rect::new(
471 rel.x + center(area.width, rel.width),
472 rel.y.saturating_sub(area.height),
473 area.width,
474 area.height,
475 )
476 } else {
477 offset = (offset.0, -offset.1);
478 Rect::new(
479 rel.x + center(area.width, rel.width), rel.bottom(),
481 area.width,
482 area.height,
483 )
484 }
485 }
486 PopupConstraint::AboveOrBelow(Alignment::Right, rel) => {
487 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
488 Rect::new(
489 rel.x + right(area.width, rel.width),
490 rel.y.saturating_sub(area.height),
491 area.width,
492 area.height,
493 )
494 } else {
495 offset = (offset.0, -offset.1);
496 Rect::new(
497 rel.x + right(area.width, rel.width), rel.bottom(),
499 area.width,
500 area.height,
501 )
502 }
503 }
504 PopupConstraint::BelowOrAbove(Alignment::Left, rel) => {
505 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
506 <= boundary_area.height
507 {
508 Rect::new(
509 rel.x, rel.bottom(),
511 area.width,
512 area.height,
513 )
514 } else {
515 offset = (offset.0, -offset.1);
516 Rect::new(
517 rel.x,
518 rel.y.saturating_sub(area.height),
519 area.width,
520 area.height,
521 )
522 }
523 }
524 PopupConstraint::BelowOrAbove(Alignment::Center, rel) => {
525 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
526 <= boundary_area.height
527 {
528 Rect::new(
529 rel.x + center(area.width, rel.width), rel.bottom(),
531 area.width,
532 area.height,
533 )
534 } else {
535 offset = (offset.0, -offset.1);
536 Rect::new(
537 rel.x + center(area.width, rel.width),
538 rel.y.saturating_sub(area.height),
539 area.width,
540 area.height,
541 )
542 }
543 }
544 PopupConstraint::BelowOrAbove(Alignment::Right, rel) => {
545 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
546 <= boundary_area.height
547 {
548 Rect::new(
549 rel.x + right(area.width, rel.width), rel.bottom(),
551 area.width,
552 area.height,
553 )
554 } else {
555 offset = (offset.0, -offset.1);
556 Rect::new(
557 rel.x + right(area.width, rel.width),
558 rel.y.saturating_sub(area.height),
559 area.width,
560 area.height,
561 )
562 }
563 }
564 };
565
566 area.x = area.x.saturating_add_signed(offset.0);
568 area.y = area.y.saturating_add_signed(offset.1);
569
570 if area.left() < boundary_area.left() {
572 area.x = boundary_area.left();
573 }
574 if area.right() >= boundary_area.right() {
575 let corr = area.right().saturating_sub(boundary_area.right());
576 area.x = max(boundary_area.left(), area.x.saturating_sub(corr));
577 }
578 if area.top() < boundary_area.top() {
579 area.y = boundary_area.top();
580 }
581 if area.bottom() >= boundary_area.bottom() {
582 let corr = area.bottom().saturating_sub(boundary_area.bottom());
583 area.y = max(boundary_area.top(), area.y.saturating_sub(corr));
584 }
585
586 if area.right() > boundary_area.right() {
588 let corr = area.right() - boundary_area.right();
589 area.width = area.width.saturating_sub(corr);
590 }
591 if area.bottom() > boundary_area.bottom() {
592 let corr = area.bottom() - boundary_area.bottom();
593 area.height = area.height.saturating_sub(corr);
594 }
595
596 area
597 }
598}
599
600impl Default for PopupStyle {
601 fn default() -> Self {
602 Self {
603 style: Default::default(),
604 offset: None,
605 block: None,
606 border_style: None,
607 scroll: None,
608 alignment: None,
609 placement: None,
610 non_exhaustive: NonExhaustive,
611 }
612 }
613}
614
615impl Clone for PopupCoreState {
616 fn clone(&self) -> Self {
617 Self {
618 area: self.area,
619 area_z: self.area_z,
620 widget_area: self.widget_area,
621 h_scroll: self.h_scroll.clone(),
622 v_scroll: self.v_scroll.clone(),
623 active: FocusFlag::named(self.active.name()),
624 mouse: Default::default(),
625 non_exhaustive: NonExhaustive,
626 }
627 }
628}
629
630impl Default for PopupCoreState {
631 fn default() -> Self {
632 Self {
633 area: Default::default(),
634 area_z: 1,
635 widget_area: Default::default(),
636 h_scroll: Default::default(),
637 v_scroll: Default::default(),
638 active: FocusFlag::named("popup"),
639 mouse: Default::default(),
640 non_exhaustive: NonExhaustive,
641 }
642 }
643}
644
645impl RelocatableState for PopupCoreState {
646 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
647 self.area = relocate_area(self.area, shift, clip);
648 self.widget_area = relocate_area(self.widget_area, shift, clip);
649 }
650}
651
652impl PopupCoreState {
653 #[inline]
655 pub fn new() -> Self {
656 Default::default()
657 }
658
659 pub fn named(name: &str) -> Self {
661 Self {
662 active: FocusFlag::named(name),
663 ..Default::default()
664 }
665 }
666
667 pub fn set_area_z(&mut self, z: u16) {
669 self.area_z = z;
670 }
671
672 pub fn area_z(&self) -> u16 {
674 self.area_z
675 }
676
677 pub fn is_active(&self) -> bool {
679 self.active.is_focused()
680 }
681
682 pub fn flip_active(&mut self) {
684 self.set_active(!self.is_active());
685 }
686
687 pub fn set_active(&mut self, active: bool) -> bool {
691 let old_value = self.is_active();
692 if active {
693 if !self.is_active() {
694 self.active.set(true);
695 self.active.set_gained(true);
696 self.active.set_lost(false);
697 } else {
698 self.active.set_gained(false);
699 self.active.set_lost(false);
700 }
701 } else {
702 if self.is_active() {
703 self.active.set(false);
704 self.active.set_gained(false);
705 self.active.set_lost(true);
706 } else {
707 self.active.set_gained(false);
708 self.active.set_lost(false);
709 }
710 }
711 old_value != self.is_active()
712 }
713
714 pub fn clear_areas(&mut self) {
716 self.area = Default::default();
717 self.widget_area = Default::default();
718 self.v_scroll.area = Default::default();
719 self.h_scroll.area = Default::default();
720 }
721}
722
723impl HandleEvent<crossterm::event::Event, Popup, PopupOutcome> for PopupCoreState {
724 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> PopupOutcome {
725 if self.is_active() {
726 match event {
727 ct_event!(mouse down Left for x,y)
728 | ct_event!(mouse down Right for x,y)
729 | ct_event!(mouse down Middle for x,y)
730 if !self.area.contains((*x, *y).into()) =>
731 {
732 PopupOutcome::Hide
733 }
734 _ => PopupOutcome::Continue,
735 }
736 } else {
737 PopupOutcome::Continue
738 }
739 }
740}