use crate::Placement;
use crate::_private::NonExhaustive;
use crate::event::PopupOutcome;
use rat_event::util::MouseFlags;
use rat_event::{ct_event, HandleEvent, Popup};
use rat_focus::{ContainerFlag, FocusContainer, ZRect};
use ratatui::buffer::Buffer;
use ratatui::layout::{Rect, Size};
use ratatui::style::Style;
use ratatui::widgets::{block::BlockExt, Block, StatefulWidget, Widget};
#[cfg(feature = "unstable-widget-ref")]
use ratatui::widgets::{StatefulWidgetRef, WidgetRef};
use std::cmp::max;
#[derive(Debug, Clone)]
pub struct PopupCore<'a> {
style: Style,
placement: Placement,
offset: (i16, i16),
boundary_area: Option<Rect>,
block: Option<Block<'a>>,
}
#[derive(Debug, Clone)]
pub struct PopupCoreState {
pub area: Rect,
pub z_areas: [ZRect; 1],
pub widget_area: Rect,
pub active: ContainerFlag,
pub mouse: MouseFlags,
pub non_exhaustive: NonExhaustive,
}
impl<'a> Default for PopupCore<'a> {
fn default() -> Self {
Self {
style: Default::default(),
placement: Placement::None,
offset: (0, 0),
boundary_area: None,
block: None,
}
}
}
impl<'a> PopupCore<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn placement(mut self, placement: Placement) -> Self {
self.placement = placement;
self
}
pub fn offset(mut self, offset: (i16, i16)) -> Self {
self.offset = offset;
self
}
pub fn x_offset(mut self, offset: i16) -> Self {
self.offset.0 = offset;
self
}
pub fn y_offset(mut self, offset: i16) -> Self {
self.offset.1 = offset;
self
}
pub fn boundary(mut self, boundary: Rect) -> Self {
self.boundary_area = Some(boundary);
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
pub fn get_block_size(&self) -> Size {
let area = Rect::new(0, 0, 100, 100);
let inner = self.block.as_ref().map_or(area, |v| v.inner(area));
Size::new(area.width - inner.width, area.height - inner.height)
}
}
#[cfg(feature = "unstable-widget-ref")]
impl<'a> StatefulWidgetRef for PopupCore<'a> {
type State = PopupCoreState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if !state.active.is_container_focused() {
state.clear_areas();
return;
}
self.layout(area, self.boundary_area.unwrap_or(buf.area), state);
for y in state.area.top()..state.area.bottom() {
for x in state.area.left()..state.area.right() {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.reset();
cell.set_style(self.style);
}
}
}
if let Some(block) = self.block.as_ref() {
block.clone().style(self.style).render_ref(state.area, buf);
}
}
}
impl<'a> StatefulWidget for PopupCore<'a> {
type State = PopupCoreState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if !state.active.is_container_focused() {
state.clear_areas();
return;
}
self.layout(area, self.boundary_area.unwrap_or(buf.area), state);
for y in state.area.top()..state.area.bottom() {
for x in state.area.left()..state.area.right() {
if let Some(cell) = buf.cell_mut((x, y)) {
cell.reset();
}
}
}
if let Some(block) = self.block {
block.style(self.style).render(state.area, buf);
}
}
}
impl<'a> PopupCore<'a> {
fn layout(&self, area: Rect, boundary_area: Rect, state: &mut PopupCoreState) {
fn center(len: u16, within: u16) -> u16 {
((within as i32 - len as i32) / 2).clamp(0, i16::MAX as i32) as u16
}
let middle = center;
fn right(len: u16, within: u16) -> u16 {
within.saturating_sub(len)
}
let bottom = right;
let mut area = match self.placement {
Placement::None => area,
Placement::AboveLeft(rel) => Rect::new(
rel.x,
rel.y.saturating_sub(area.height),
area.width,
area.height,
),
Placement::AboveCenter(rel) => Rect::new(
rel.x + center(area.width, rel.width),
rel.y.saturating_sub(area.height),
area.width,
area.height,
),
Placement::AboveRight(rel) => Rect::new(
rel.x + right(area.width, rel.width),
rel.y.saturating_sub(area.height),
area.width,
area.height,
),
Placement::BelowLeft(rel) => Rect::new(rel.x, rel.bottom(), area.width, area.height),
Placement::BelowCenter(rel) => Rect::new(
rel.x + center(area.width, rel.width),
rel.bottom(),
area.width,
area.height,
),
Placement::BelowRight(rel) => Rect::new(
rel.x + right(area.width, rel.width),
rel.bottom(),
area.width,
area.height,
),
Placement::LeftTop(rel) => Rect::new(
rel.x.saturating_sub(area.width),
rel.y,
area.width,
area.height,
),
Placement::LeftMiddle(rel) => Rect::new(
rel.x.saturating_sub(area.width),
rel.y + middle(area.height, rel.height),
area.width,
area.height,
),
Placement::LeftBottom(rel) => Rect::new(
rel.x.saturating_sub(area.width),
rel.y + bottom(area.height, rel.height),
area.width,
area.height,
),
Placement::RightTop(rel) => Rect::new(rel.right(), rel.y, area.width, area.height),
Placement::RightMiddle(rel) => Rect::new(
rel.right(),
rel.y + middle(area.height, rel.height),
area.width,
area.height,
),
Placement::RightBottom(rel) => Rect::new(
rel.right(),
rel.y + bottom(area.height, rel.height),
area.width,
area.height,
),
Placement::Position(x, y) => Rect::new(x, y, area.width, area.height),
};
area.x = area.x.saturating_add_signed(self.offset.0);
area.y = area.y.saturating_add_signed(self.offset.1);
if area.left() < boundary_area.left() {
let corr = boundary_area.left().saturating_sub(area.left());
area.x += corr;
}
if area.right() >= boundary_area.right() {
let corr = area.right().saturating_sub(boundary_area.right());
area.x = area.x.saturating_sub(corr);
}
if area.top() < boundary_area.top() {
let corr = boundary_area.top().saturating_sub(area.top());
area.y += corr;
}
if area.bottom() >= boundary_area.bottom() {
let corr = area.bottom().saturating_sub(boundary_area.bottom());
area.y = area.y.saturating_sub(corr);
}
if area.right() > boundary_area.right() {
let corr = area.right() - boundary_area.right();
area.width = area.width.saturating_sub(corr);
}
if area.bottom() > boundary_area.bottom() {
let corr = area.bottom() - boundary_area.bottom();
area.height = area.height.saturating_sub(corr);
}
state.area = area;
state.widget_area = self.block.inner_if_some(area);
state.z_areas[0] = ZRect::from((1, area));
}
}
impl Default for PopupCoreState {
fn default() -> Self {
Self {
area: Default::default(),
z_areas: [Default::default()],
widget_area: Default::default(),
active: ContainerFlag::named("popup"),
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl PopupCoreState {
#[inline]
pub fn new() -> Self {
Default::default()
}
pub fn named(name: &str) -> Self {
Self {
active: ContainerFlag::named(name),
..Default::default()
}
}
pub fn is_active(&self) -> bool {
self.active.is_container_focused()
}
pub fn flip_active(&mut self) {
self.set_active(!self.active.get());
}
pub fn set_active(&mut self, active: bool) {
self.active.set(active);
if !active {
self.active.clear();
self.clear_areas();
}
}
pub fn clear_areas(&mut self) {
self.area = Default::default();
self.widget_area = Default::default();
self.z_areas = Default::default();
}
}
impl HandleEvent<crossterm::event::Event, Popup, PopupOutcome> for PopupCoreState {
fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> PopupOutcome {
let r0 = if self.active.container_lost_focus() {
PopupOutcome::Hide
} else {
PopupOutcome::Continue
};
let r1 = if self.is_active() {
match event {
ct_event!(mouse down Left for x,y)
| ct_event!(mouse down Right for x,y)
| ct_event!(mouse down Middle for x,y)
if !self.area.contains((*x, *y).into()) =>
{
PopupOutcome::Hide
}
_ => PopupOutcome::Continue,
}
} else {
PopupOutcome::Continue
};
max(r0, r1)
}
}