use crate::_private::NonExhaustive;
use crate::Outcome;
use ratatui_core::layout::{Position, Rect};
use ratatui_crossterm::crossterm::event::{
Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::time::SystemTime;
pub fn item_at(areas: &[Rect], x_pos: u16, y_pos: u16) -> Option<usize> {
for (i, r) in areas.iter().enumerate() {
if y_pos >= r.top() && y_pos < r.bottom() && x_pos >= r.left() && x_pos < r.right() {
return Some(i);
}
}
None
}
pub fn row_at(areas: &[Rect], y_pos: u16) -> Option<usize> {
for (i, r) in areas.iter().enumerate() {
if y_pos >= r.top() && y_pos < r.bottom() {
return Some(i);
}
}
None
}
pub fn column_at(areas: &[Rect], x_pos: u16) -> Option<usize> {
for (i, r) in areas.iter().enumerate() {
if x_pos >= r.left() && x_pos < r.right() {
return Some(i);
}
}
None
}
pub fn row_at_drag(encompassing: Rect, areas: &[Rect], y_pos: u16) -> Result<usize, isize> {
if let Some(row) = row_at(areas, y_pos) {
return Ok(row);
}
#[allow(clippy::collapsible_else_if)]
if y_pos < encompassing.top() {
Err(y_pos as isize - encompassing.top() as isize)
} else {
if let Some(last) = areas.last() {
Err(y_pos as isize - last.bottom() as isize + 1)
} else {
Err(y_pos as isize - encompassing.top() as isize)
}
}
}
pub fn column_at_drag(encompassing: Rect, areas: &[Rect], x_pos: u16) -> Result<usize, isize> {
if let Some(column) = column_at(areas, x_pos) {
return Ok(column);
}
#[allow(clippy::collapsible_else_if)]
if x_pos < encompassing.left() {
Err(x_pos as isize - encompassing.left() as isize)
} else {
if let Some(last) = areas.last() {
Err(x_pos as isize - last.right() as isize + 1)
} else {
Err(x_pos as isize - encompassing.left() as isize)
}
}
}
pub fn mouse_trap(event: &Event, area: Rect) -> Outcome {
match event {
Event::Mouse(MouseEvent {
kind:
MouseEventKind::ScrollLeft
| MouseEventKind::ScrollRight
| MouseEventKind::ScrollUp
| MouseEventKind::ScrollDown
| MouseEventKind::Down(_)
| MouseEventKind::Up(_)
| MouseEventKind::Moved,
column,
row,
..
}) if area.contains(Position::new(*column, *row)) => Outcome::Unchanged,
_ => Outcome::Continue,
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Clicks {
#[default]
None,
Down1(usize),
Up1(usize),
Down2(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MouseFlags {
pub time: Cell<Option<SystemTime>>,
pub click: Cell<Clicks>,
pub drag: Cell<bool>,
pub hover: Cell<bool>,
pub non_exhaustive: NonExhaustive,
}
impl Default for MouseFlags {
fn default() -> Self {
Self {
time: Default::default(),
click: Default::default(),
drag: Default::default(),
hover: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl MouseFlags {
pub fn pos_of(&self, event: &MouseEvent) -> (u16, u16) {
(event.column, event.row)
}
pub fn item_at(&self, areas: &[Rect], x_pos: u16, y_pos: u16) -> Option<usize> {
item_at(areas, x_pos, y_pos)
}
pub fn row_at(&self, areas: &[Rect], y_pos: u16) -> Option<usize> {
row_at(areas, y_pos)
}
pub fn column_at(&self, areas: &[Rect], x_pos: u16) -> Option<usize> {
column_at(areas, x_pos)
}
pub fn row_at_drag(
&self,
encompassing: Rect,
areas: &[Rect],
y_pos: u16,
) -> Result<usize, isize> {
row_at_drag(encompassing, areas, y_pos)
}
pub fn column_at_drag(
&self,
encompassing: Rect,
areas: &[Rect],
x_pos: u16,
) -> Result<usize, isize> {
column_at_drag(encompassing, areas, x_pos)
}
pub fn hover(&self, area: Rect, event: &MouseEvent) -> bool {
match event {
MouseEvent {
kind: MouseEventKind::Moved,
column,
row,
modifiers: KeyModifiers::NONE,
} => {
let old_hover = self.hover.get();
if area.contains((*column, *row).into()) {
self.hover.set(true);
} else {
self.hover.set(false);
}
old_hover != self.hover.get()
}
_ => false,
}
}
pub fn drag(&self, area: Rect, event: &MouseEvent) -> bool {
self.drag2(area, event, KeyModifiers::NONE)
}
pub fn drag2(&self, area: Rect, event: &MouseEvent, filter: KeyModifiers) -> bool {
match event {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row,
modifiers,
} if *modifiers == filter => {
if area.contains((*column, *row).into()) {
self.drag.set(true);
} else {
self.drag.set(false);
}
}
MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
modifiers,
..
} if *modifiers == filter => {
if self.drag.get() {
return true;
}
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left) | MouseEventKind::Moved,
..
} => {
self.drag.set(false);
}
_ => {}
}
false
}
pub fn doubleclick(&self, area: Rect, event: &MouseEvent) -> bool {
self.doubleclick2(area, event, KeyModifiers::NONE)
}
#[allow(clippy::collapsible_if)]
pub fn doubleclick2(&self, area: Rect, event: &MouseEvent, filter: KeyModifiers) -> bool {
match event {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row,
modifiers,
} if *modifiers == filter => 'f: {
if area.contains((*column, *row).into()) {
match self.click.get() {
Clicks::Up1(_) => {
if let Some(time) = self.time.get() {
if time.elapsed().unwrap_or_default().as_millis() as u32
> double_click_timeout()
{
self.time.set(Some(SystemTime::now()));
self.click.set(Clicks::Down1(0));
break 'f false;
}
}
}
_ => {
self.time.set(Some(SystemTime::now()));
self.click.set(Clicks::Down1(0));
}
}
break 'f false;
} else {
self.time.set(None);
self.click.set(Clicks::None);
break 'f false;
}
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column,
row,
modifiers,
} if *modifiers == filter => 'f: {
if area.contains((*column, *row).into()) {
match self.click.get() {
Clicks::Down1(_) => {
self.click.set(Clicks::Up1(0));
break 'f false;
}
Clicks::Up1(_) => {
self.click.set(Clicks::None);
break 'f true;
}
Clicks::Down2(_) => {
self.click.set(Clicks::None);
break 'f true;
}
_ => {
self.click.set(Clicks::None);
break 'f false;
}
}
} else {
self.click.set(Clicks::None);
break 'f false;
}
}
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MouseFlagsN {
pub time: Cell<Option<SystemTime>>,
pub click: Cell<Clicks>,
pub drag: Cell<Option<usize>>,
pub hover: Cell<Option<usize>>,
pub non_exhaustive: NonExhaustive,
}
impl Default for MouseFlagsN {
fn default() -> Self {
Self {
time: Default::default(),
click: Default::default(),
drag: Default::default(),
hover: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl MouseFlagsN {
pub fn pos_of(&self, event: &MouseEvent) -> (u16, u16) {
(event.column, event.row)
}
pub fn item_at(&self, areas: &[Rect], x_pos: u16, y_pos: u16) -> Option<usize> {
item_at(areas, x_pos, y_pos)
}
pub fn row_at(&self, areas: &[Rect], y_pos: u16) -> Option<usize> {
row_at(areas, y_pos)
}
pub fn column_at(&self, areas: &[Rect], x_pos: u16) -> Option<usize> {
column_at(areas, x_pos)
}
pub fn row_at_drag(
&self,
encompassing: Rect,
areas: &[Rect],
y_pos: u16,
) -> Result<usize, isize> {
row_at_drag(encompassing, areas, y_pos)
}
pub fn column_at_drag(
&self,
encompassing: Rect,
areas: &[Rect],
x_pos: u16,
) -> Result<usize, isize> {
column_at_drag(encompassing, areas, x_pos)
}
pub fn hover(&self, areas: &[Rect], event: &MouseEvent) -> bool {
match event {
MouseEvent {
kind: MouseEventKind::Moved,
column,
row,
modifiers: KeyModifiers::NONE,
} => {
let old_hover = self.hover.get();
if let Some(n) = self.item_at(areas, *column, *row) {
self.hover.set(Some(n));
} else {
self.hover.set(None);
}
old_hover != self.hover.get()
}
_ => false,
}
}
pub fn drag(&self, areas: &[Rect], event: &MouseEvent) -> bool {
self.drag2(areas, event, KeyModifiers::NONE)
}
pub fn drag2(&self, areas: &[Rect], event: &MouseEvent, filter: KeyModifiers) -> bool {
match event {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row,
modifiers,
} if *modifiers == filter => {
self.drag.set(None);
for (n, area) in areas.iter().enumerate() {
if area.contains((*column, *row).into()) {
self.drag.set(Some(n));
}
}
}
MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
modifiers,
..
} if *modifiers == filter => {
if self.drag.get().is_some() {
return true;
}
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left) | MouseEventKind::Moved,
..
} => {
self.drag.set(None);
}
_ => {}
}
false
}
pub fn doubleclick(&self, areas: &[Rect], event: &MouseEvent) -> bool {
self.doubleclick2(areas, event, KeyModifiers::NONE)
}
#[allow(clippy::collapsible_if)]
pub fn doubleclick2(&self, areas: &[Rect], event: &MouseEvent, filter: KeyModifiers) -> bool {
match event {
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row,
modifiers,
} if *modifiers == filter => 'f: {
for (n, area) in areas.iter().enumerate() {
if area.contains((*column, *row).into()) {
match self.click.get() {
Clicks::Up1(v) => {
if let Some(time) = self.time.get() {
if time.elapsed().unwrap_or_default().as_millis() as u32
> double_click_timeout()
{
self.time.set(Some(SystemTime::now()));
self.click.set(Clicks::Down1(n));
break 'f false;
}
}
if n == v {
self.click.set(Clicks::Down2(n));
} else {
self.click.set(Clicks::None);
}
}
_ => {
self.time.set(Some(SystemTime::now()));
self.click.set(Clicks::Down1(n));
}
}
break 'f false;
}
}
self.time.set(None);
self.click.set(Clicks::None);
false
}
MouseEvent {
kind: MouseEventKind::Up(MouseButton::Left),
column,
row,
modifiers,
} if *modifiers == filter => 'f: {
for (n, area) in areas.iter().enumerate() {
if area.contains((*column, *row).into()) {
match self.click.get() {
Clicks::Down1(v) => {
if n == v {
self.click.set(Clicks::Up1(v));
} else {
self.click.set(Clicks::None);
}
}
Clicks::Up1(v) => {
if n == v {
self.click.set(Clicks::None);
break 'f true;
} else {
self.click.set(Clicks::None);
}
}
Clicks::Down2(v) => {
if n == v {
self.click.set(Clicks::None);
break 'f true;
} else {
self.click.set(Clicks::None);
}
}
_ => {
self.click.set(Clicks::None);
}
}
break 'f false;
}
}
self.click.set(Clicks::None);
false
}
_ => false,
}
}
}
static DOUBLE_CLICK: AtomicU32 = AtomicU32::new(250);
pub fn set_double_click_timeout(timeout: u32) {
DOUBLE_CLICK.store(timeout, Ordering::Release);
}
pub fn double_click_timeout() -> u32 {
DOUBLE_CLICK.load(Ordering::Acquire)
}
static ENHANCED_KEYS: AtomicBool = AtomicBool::new(false);
pub fn have_keyboard_enhancement() -> bool {
ENHANCED_KEYS.load(Ordering::Acquire)
}
pub fn set_have_keyboard_enhancement(have: bool) {
ENHANCED_KEYS.store(have, Ordering::Release);
}