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::buffer::Buffer;
8use ratatui::layout::{Alignment, Rect};
9use ratatui::style::{Style, Stylize};
10use ratatui::widgets::StatefulWidget;
11use std::cell::Cell;
12use std::cmp::max;
13use std::rc::Rc;
14
15#[derive(Debug, Clone)]
36pub struct PopupCore {
37 pub constraint: Cell<PopupConstraint>,
39 pub offset: (i16, i16),
42 pub boundary_area: Option<Rect>,
45
46 pub non_exhaustive: NonExhaustive,
47}
48
49#[derive(Debug, Clone)]
51pub struct PopupStyle {
52 pub offset: Option<(i16, i16)>,
54 pub alignment: Option<Alignment>,
56 pub placement: Option<Placement>,
58
59 pub non_exhaustive: NonExhaustive,
61}
62
63#[derive(Debug)]
65pub struct PopupCoreState {
66 pub area: Rect,
71 pub area_z: u16,
73
74 pub active: Rc<Cell<bool>>,
78
79 pub mouse: MouseFlags,
82
83 pub non_exhaustive: NonExhaustive,
85}
86
87impl Default for PopupCore {
88 fn default() -> Self {
89 Self {
90 constraint: Cell::new(PopupConstraint::None),
91 offset: (0, 0),
92 boundary_area: None,
93 non_exhaustive: NonExhaustive,
94 }
95 }
96}
97
98impl PopupCore {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn ref_constraint(&self, constraint: PopupConstraint) -> &Self {
106 self.constraint.set(constraint);
107 self
108 }
109
110 pub fn constraint(self, constraint: PopupConstraint) -> Self {
112 self.constraint.set(constraint);
113 self
114 }
115
116 pub fn offset(mut self, offset: (i16, i16)) -> Self {
123 self.offset = offset;
124 self
125 }
126
127 pub fn x_offset(mut self, offset: i16) -> Self {
130 self.offset.0 = offset;
131 self
132 }
133
134 pub fn y_offset(mut self, offset: i16) -> Self {
137 self.offset.1 = offset;
138 self
139 }
140
141 pub fn boundary(mut self, boundary: Rect) -> Self {
149 self.boundary_area = Some(boundary);
150 self
151 }
152
153 pub fn styles(mut self, styles: PopupStyle) -> Self {
155 if let Some(offset) = styles.offset {
156 self.offset = offset;
157 }
158
159 self
160 }
161
162 pub fn layout(&self, area: Rect, buf: &Buffer) -> Rect {
164 self._layout(area, self.boundary_area.unwrap_or(buf.area))
165 }
166}
167
168impl StatefulWidget for &PopupCore {
169 type State = PopupCoreState;
170
171 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
172 render_popup(self, area, buf, state);
173 }
174}
175
176impl StatefulWidget for PopupCore {
177 type State = PopupCoreState;
178
179 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
180 render_popup(&self, area, buf, state);
181 }
182}
183
184fn render_popup(widget: &PopupCore, area: Rect, buf: &mut Buffer, state: &mut PopupCoreState) {
185 if !state.active.get() {
186 state.clear_areas();
187 return;
188 }
189
190 state.area = widget._layout(area, widget.boundary_area.unwrap_or(buf.area));
191
192 reset_buf_area(state.area, buf);
193}
194
195pub fn fallback_popup_style(style: Style) -> Style {
197 if style.fg.is_some() || style.bg.is_some() {
198 style
199 } else {
200 style.black().on_gray()
201 }
202}
203
204pub fn reset_buf_area(area: Rect, buf: &mut Buffer) {
206 for y in area.top()..area.bottom() {
207 for x in area.left()..area.right() {
208 if let Some(cell) = buf.cell_mut((x, y)) {
209 cell.reset();
210 }
211 }
212 }
213}
214
215impl PopupCore {
216 fn _layout(&self, area: Rect, boundary_area: Rect) -> Rect {
217 fn center(len: u16, within: u16) -> u16 {
219 ((within as i32 - len as i32) / 2).clamp(0, i16::MAX as i32) as u16
220 }
221 let middle = center;
222 fn right(len: u16, within: u16) -> u16 {
223 within.saturating_sub(len)
224 }
225 let bottom = right;
226
227 let mut offset = self.offset;
229
230 let mut area = match self.constraint.get() {
231 PopupConstraint::None => area,
232 PopupConstraint::Above(Alignment::Left, rel) => Rect::new(
233 rel.x,
234 rel.y.saturating_sub(area.height),
235 area.width,
236 area.height,
237 ),
238 PopupConstraint::Above(Alignment::Center, rel) => Rect::new(
239 rel.x + center(area.width, rel.width),
240 rel.y.saturating_sub(area.height),
241 area.width,
242 area.height,
243 ),
244 PopupConstraint::Above(Alignment::Right, rel) => Rect::new(
245 rel.x + right(area.width, rel.width),
246 rel.y.saturating_sub(area.height),
247 area.width,
248 area.height,
249 ),
250 PopupConstraint::Below(Alignment::Left, rel) => Rect::new(
251 rel.x, rel.bottom(),
253 area.width,
254 area.height,
255 ),
256 PopupConstraint::Below(Alignment::Center, rel) => Rect::new(
257 rel.x + center(area.width, rel.width),
258 rel.bottom(),
259 area.width,
260 area.height,
261 ),
262 PopupConstraint::Below(Alignment::Right, rel) => Rect::new(
263 rel.x + right(area.width, rel.width),
264 rel.bottom(),
265 area.width,
266 area.height,
267 ),
268
269 PopupConstraint::Left(Alignment::Left, rel) => Rect::new(
270 rel.x.saturating_sub(area.width),
271 rel.y,
272 area.width,
273 area.height,
274 ),
275 PopupConstraint::Left(Alignment::Center, rel) => Rect::new(
276 rel.x.saturating_sub(area.width),
277 rel.y + middle(area.height, rel.height),
278 area.width,
279 area.height,
280 ),
281 PopupConstraint::Left(Alignment::Right, rel) => Rect::new(
282 rel.x.saturating_sub(area.width),
283 rel.y + bottom(area.height, rel.height),
284 area.width,
285 area.height,
286 ),
287 PopupConstraint::Right(Alignment::Left, rel) => Rect::new(
288 rel.right(), rel.y,
290 area.width,
291 area.height,
292 ),
293 PopupConstraint::Right(Alignment::Center, rel) => Rect::new(
294 rel.right(),
295 rel.y + middle(area.height, rel.height),
296 area.width,
297 area.height,
298 ),
299 PopupConstraint::Right(Alignment::Right, rel) => Rect::new(
300 rel.right(),
301 rel.y + bottom(area.height, rel.height),
302 area.width,
303 area.height,
304 ),
305
306 PopupConstraint::Position(x, y) => Rect::new(
307 x, y,
309 area.width,
310 area.height,
311 ),
312
313 PopupConstraint::AboveOrBelow(Alignment::Left, rel) => {
314 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
315 Rect::new(
316 rel.x,
317 rel.y.saturating_sub(area.height),
318 area.width,
319 area.height,
320 )
321 } else {
322 offset = (offset.0, -offset.1);
323 Rect::new(
324 rel.x, rel.bottom(),
326 area.width,
327 area.height,
328 )
329 }
330 }
331 PopupConstraint::AboveOrBelow(Alignment::Center, rel) => {
332 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
333 Rect::new(
334 rel.x + center(area.width, rel.width),
335 rel.y.saturating_sub(area.height),
336 area.width,
337 area.height,
338 )
339 } else {
340 offset = (offset.0, -offset.1);
341 Rect::new(
342 rel.x + center(area.width, rel.width), rel.bottom(),
344 area.width,
345 area.height,
346 )
347 }
348 }
349 PopupConstraint::AboveOrBelow(Alignment::Right, rel) => {
350 if area.height.saturating_add_signed(-self.offset.1) < rel.y {
351 Rect::new(
352 rel.x + right(area.width, rel.width),
353 rel.y.saturating_sub(area.height),
354 area.width,
355 area.height,
356 )
357 } else {
358 offset = (offset.0, -offset.1);
359 Rect::new(
360 rel.x + right(area.width, rel.width), rel.bottom(),
362 area.width,
363 area.height,
364 )
365 }
366 }
367 PopupConstraint::BelowOrAbove(Alignment::Left, rel) => {
368 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
369 <= boundary_area.height
370 {
371 Rect::new(
372 rel.x, rel.bottom(),
374 area.width,
375 area.height,
376 )
377 } else {
378 offset = (offset.0, -offset.1);
379 Rect::new(
380 rel.x,
381 rel.y.saturating_sub(area.height),
382 area.width,
383 area.height,
384 )
385 }
386 }
387 PopupConstraint::BelowOrAbove(Alignment::Center, rel) => {
388 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
389 <= boundary_area.height
390 {
391 Rect::new(
392 rel.x + center(area.width, rel.width), rel.bottom(),
394 area.width,
395 area.height,
396 )
397 } else {
398 offset = (offset.0, -offset.1);
399 Rect::new(
400 rel.x + center(area.width, rel.width),
401 rel.y.saturating_sub(area.height),
402 area.width,
403 area.height,
404 )
405 }
406 }
407 PopupConstraint::BelowOrAbove(Alignment::Right, rel) => {
408 if (rel.bottom() + area.height).saturating_add_signed(self.offset.1)
409 <= boundary_area.height
410 {
411 Rect::new(
412 rel.x + right(area.width, rel.width), rel.bottom(),
414 area.width,
415 area.height,
416 )
417 } else {
418 offset = (offset.0, -offset.1);
419 Rect::new(
420 rel.x + right(area.width, rel.width),
421 rel.y.saturating_sub(area.height),
422 area.width,
423 area.height,
424 )
425 }
426 }
427 };
428
429 area.x = area.x.saturating_add_signed(offset.0);
431 area.y = area.y.saturating_add_signed(offset.1);
432
433 if area.left() < boundary_area.left() {
435 area.x = boundary_area.left();
436 }
437 if area.right() >= boundary_area.right() {
438 let corr = area.right().saturating_sub(boundary_area.right());
439 area.x = max(boundary_area.left(), area.x.saturating_sub(corr));
440 }
441 if area.top() < boundary_area.top() {
442 area.y = boundary_area.top();
443 }
444 if area.bottom() >= boundary_area.bottom() {
445 let corr = area.bottom().saturating_sub(boundary_area.bottom());
446 area.y = max(boundary_area.top(), area.y.saturating_sub(corr));
447 }
448
449 if area.right() > boundary_area.right() {
451 let corr = area.right() - boundary_area.right();
452 area.width = area.width.saturating_sub(corr);
453 }
454 if area.bottom() > boundary_area.bottom() {
455 let corr = area.bottom() - boundary_area.bottom();
456 area.height = area.height.saturating_sub(corr);
457 }
458
459 area
460 }
461}
462
463impl Default for PopupStyle {
464 fn default() -> Self {
465 Self {
466 offset: None,
467 alignment: None,
468 placement: None,
469 non_exhaustive: NonExhaustive,
470 }
471 }
472}
473
474impl Clone for PopupCoreState {
475 fn clone(&self) -> Self {
476 Self {
477 area: self.area,
478 area_z: self.area_z,
479 active: self.active.clone(),
480 mouse: Default::default(),
481 non_exhaustive: NonExhaustive,
482 }
483 }
484}
485
486impl Default for PopupCoreState {
487 fn default() -> Self {
488 Self {
489 area: Default::default(),
490 area_z: 1,
491 active: Default::default(),
492 mouse: Default::default(),
493 non_exhaustive: NonExhaustive,
494 }
495 }
496}
497
498impl RelocatableState for PopupCoreState {
499 fn relocate(&mut self, _shift: (i16, i16), _clip: Rect) {}
500
501 fn relocate_popup(&mut self, shift: (i16, i16), clip: Rect) {
502 self.area.relocate(shift, clip);
503 }
504}
505
506impl PopupCoreState {
507 #[inline]
509 pub fn new() -> Self {
510 Default::default()
511 }
512
513 pub fn is_active(&self) -> bool {
515 self.active.get()
516 }
517
518 pub fn flip_active(&mut self) {
520 self.set_active(!self.is_active());
521 }
522
523 pub fn set_active(&mut self, active: bool) -> bool {
527 let old_value = self.is_active();
528 self.active.set(active);
529 old_value != self.is_active()
530 }
531
532 pub fn clear_areas(&mut self) {
534 self.area = Default::default();
535 }
536}
537
538impl HandleEvent<crossterm::event::Event, Popup, PopupOutcome> for PopupCoreState {
539 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> PopupOutcome {
540 if self.is_active() {
541 match event {
542 ct_event!(mouse down Left for x,y)
543 | ct_event!(mouse down Right for x,y)
544 | ct_event!(mouse down Middle for x,y)
545 if !self.area.contains((*x, *y).into()) =>
546 {
547 PopupOutcome::Hide
548 }
549 _ => PopupOutcome::Continue,
550 }
551 } else {
552 PopupOutcome::Continue
553 }
554 }
555}