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