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