use crossterm::event::{
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
use ratatui_core::layout::{Position, Rect};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Outcome {
#[default]
Ignored,
Consumed,
Changed,
}
impl Outcome {
pub fn is_ignored(self) -> bool {
self == Outcome::Ignored
}
pub fn is_consumed(self) -> bool {
self != Outcome::Ignored
}
pub fn is_changed(self) -> bool {
self == Outcome::Changed
}
pub fn changed_if(flag: bool) -> Outcome {
if flag {
Outcome::Changed
} else {
Outcome::Consumed
}
}
}
impl std::ops::BitOr for Outcome {
type Output = Outcome;
fn bitor(self, rhs: Outcome) -> Outcome {
self.max(rhs)
}
}
impl std::ops::BitOrAssign for Outcome {
fn bitor_assign(&mut self, rhs: Outcome) {
*self = (*self).max(rhs);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Look {
pub focused: bool,
pub hover: bool,
pub enabled: bool,
}
impl Default for Look {
fn default() -> Self {
Look::PLAIN
}
}
impl Look {
pub const PLAIN: Look = Look {
focused: false,
hover: false,
enabled: true,
};
pub const fn new() -> Self {
Look::PLAIN
}
pub const fn focused(mut self, v: bool) -> Self {
self.focused = v;
self
}
pub const fn hover(mut self, v: bool) -> Self {
self.hover = v;
self
}
pub const fn enabled(mut self, v: bool) -> Self {
self.enabled = v;
self
}
pub const fn hovering(&self) -> bool {
self.hover && self.enabled
}
}
#[derive(Clone, Debug)]
pub struct Focus<T> {
order: Vec<T>,
current: usize,
pub wrap: bool,
}
impl<T: Copy + PartialEq> Focus<T> {
pub fn new(order: impl Into<Vec<T>>) -> Self {
Focus {
order: order.into(),
current: 0,
wrap: true,
}
}
pub fn set_order(&mut self, order: impl Into<Vec<T>>) {
let cur = self.current();
self.order = order.into();
self.current = cur
.and_then(|c| self.order.iter().position(|&x| x == c))
.unwrap_or(0);
}
pub fn order(&self) -> &[T] {
&self.order
}
pub fn current(&self) -> Option<T> {
self.order.get(self.current).copied()
}
pub fn is(&self, id: T) -> bool {
self.current() == Some(id)
}
pub fn set(&mut self, id: T) -> bool {
match self.order.iter().position(|&x| x == id) {
Some(i) => {
self.current = i;
true
}
None => false,
}
}
pub fn index(&self) -> usize {
self.current
}
pub fn set_index(&mut self, i: usize) {
if !self.order.is_empty() {
self.current = i.min(self.order.len() - 1);
}
}
pub fn next(&mut self) {
self.step(1);
}
pub fn prev(&mut self) {
self.step(-1);
}
fn step(&mut self, d: i32) {
let n = self.order.len() as i32;
if n == 0 {
return;
}
let next = self.current as i32 + d;
self.current = if self.wrap {
next.rem_euclid(n)
} else {
next.clamp(0, n - 1)
} as usize;
}
pub fn handle_key(&mut self, key: KeyEvent) -> Outcome {
match key.code {
KeyCode::Tab => {
self.next();
Outcome::Consumed
}
KeyCode::BackTab => {
self.prev();
Outcome::Consumed
}
_ => Outcome::Ignored,
}
}
}
pub fn mouse_pos(m: &MouseEvent) -> Position {
Position {
x: m.column,
y: m.row,
}
}
pub fn mouse_in(area: Rect, m: &MouseEvent) -> bool {
area.contains(mouse_pos(m))
}
pub fn is_left_down(m: &MouseEvent) -> bool {
m.kind == MouseEventKind::Down(MouseButton::Left)
}
pub fn is_left_up(m: &MouseEvent) -> bool {
m.kind == MouseEventKind::Up(MouseButton::Left)
}
pub fn is_left_drag(m: &MouseEvent) -> bool {
m.kind == MouseEventKind::Drag(MouseButton::Left)
}
pub fn is_move(m: &MouseEvent) -> bool {
m.kind == MouseEventKind::Moved
}
pub fn wheel_delta(m: &MouseEvent) -> Option<i32> {
match m.kind {
MouseEventKind::ScrollDown => Some(1),
MouseEventKind::ScrollUp => Some(-1),
_ => None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Hit {
None,
HoverChanged,
Press,
Drag,
Click,
Cancel,
Wheel(i32),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HitBox {
pub area: Rect,
pub hover: bool,
pub pressed: bool,
}
impl HitBox {
pub fn set_area(&mut self, area: Rect) {
self.area = area;
}
pub fn contains(&self, m: &MouseEvent) -> bool {
mouse_in(self.area, m)
}
pub fn mouse(&mut self, m: &MouseEvent) -> Hit {
let inside = self.contains(m);
match m.kind {
MouseEventKind::Moved => {
if inside != self.hover {
self.hover = inside;
Hit::HoverChanged
} else {
Hit::None
}
}
MouseEventKind::Down(MouseButton::Left) => {
let was = self.hover;
self.hover = inside;
if inside {
self.pressed = true;
Hit::Press
} else if was {
Hit::HoverChanged
} else {
Hit::None
}
}
MouseEventKind::Drag(MouseButton::Left) => {
if self.pressed {
Hit::Drag
} else if inside != self.hover {
self.hover = inside;
Hit::HoverChanged
} else {
Hit::None
}
}
MouseEventKind::Up(MouseButton::Left) => {
if self.pressed {
self.pressed = false;
if inside { Hit::Click } else { Hit::Cancel }
} else {
Hit::None
}
}
MouseEventKind::ScrollDown if inside => Hit::Wheel(1),
MouseEventKind::ScrollUp if inside => Hit::Wheel(-1),
_ => Hit::None,
}
}
pub fn look(&self, focused: bool, enabled: bool) -> Look {
Look {
focused,
hover: self.hover,
enabled,
}
}
}
pub fn is_press(k: &KeyEvent) -> bool {
k.kind != KeyEventKind::Release
}
pub fn ctrl(k: &KeyEvent, c: char) -> bool {
k.modifiers.contains(KeyModifiers::CONTROL) && k.code == KeyCode::Char(c)
}
pub fn alt(k: &KeyEvent, c: char) -> bool {
k.modifiers.contains(KeyModifiers::ALT) && k.code == KeyCode::Char(c)
}
pub fn plain_char(k: &KeyEvent) -> Option<char> {
match k.code {
KeyCode::Char(c)
if !k
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) =>
{
Some(c)
}
_ => None,
}
}
pub fn is_activate(k: &KeyEvent) -> bool {
matches!(k.code, KeyCode::Enter | KeyCode::Char(' '))
}
pub fn key_label(k: &KeyEvent) -> String {
let mut s = String::new();
if k.modifiers.contains(KeyModifiers::CONTROL) {
s.push('^');
}
if k.modifiers.contains(KeyModifiers::ALT) {
s.push_str("Alt+");
}
if k.modifiers.contains(KeyModifiers::SHIFT)
&& !matches!(k.code, KeyCode::Char(_) | KeyCode::BackTab)
{
s.push('⇧');
}
match k.code {
KeyCode::Char(' ') => s.push_str("Space"),
KeyCode::Char(c) => s.push(c),
KeyCode::F(n) => s.push_str(&format!("F{n}")),
KeyCode::Esc => s.push_str("Esc"),
KeyCode::Enter => s.push_str("Enter"),
KeyCode::BackTab => s.push_str("⇧Tab"),
KeyCode::Tab => s.push_str("Tab"),
KeyCode::Backspace => s.push_str("Bksp"),
KeyCode::Up => s.push('↑'),
KeyCode::Down => s.push('↓'),
KeyCode::Left => s.push('←'),
KeyCode::Right => s.push('→'),
KeyCode::PageUp => s.push_str("PgUp"),
KeyCode::PageDown => s.push_str("PgDn"),
other => s.push_str(&format!("{other:?}")),
}
s
}
pub trait Interactive {
fn handle_key(&mut self, _key: KeyEvent) -> Outcome {
Outcome::Ignored
}
fn handle_mouse(&mut self, _m: MouseEvent) -> Outcome {
Outcome::Ignored
}
fn handle(&mut self, ev: &Event) -> Outcome {
match ev {
Event::Key(k) if is_press(k) => self.handle_key(*k),
Event::Mouse(m) => self.handle_mouse(*m),
_ => Outcome::Ignored,
}
}
}
pub fn word_boundary(graphemes: &[&str], pos: usize, forward: bool) -> usize {
let alnum = |g: &str| g.chars().all(char::is_alphanumeric);
let len = graphemes.len();
if forward {
let mut i = pos;
while i < len && !alnum(graphemes[i]) {
i += 1;
}
while i < len && alnum(graphemes[i]) {
i += 1;
}
i
} else {
let mut i = pos.saturating_sub(1);
while i > 0 && !alnum(graphemes[i]) {
i = i.saturating_sub(1);
}
while i > 0 && alnum(graphemes[i.saturating_sub(1)]) {
i = i.saturating_sub(1);
}
i
}
}
#[cfg(test)]
mod tests {
use super::*;
fn me(kind: MouseEventKind, x: u16, y: u16) -> MouseEvent {
MouseEvent {
kind,
column: x,
row: y,
modifiers: KeyModifiers::NONE,
}
}
#[test]
fn hitbox_click_requires_press_and_release_inside() {
let mut h = HitBox {
area: Rect::new(5, 5, 10, 3),
..Default::default()
};
assert_eq!(h.mouse(&me(MouseEventKind::Moved, 6, 6)), Hit::HoverChanged);
assert_eq!(h.mouse(&me(MouseEventKind::Moved, 7, 6)), Hit::None);
assert_eq!(
h.mouse(&me(MouseEventKind::Down(MouseButton::Left), 7, 6)),
Hit::Press
);
assert_eq!(
h.mouse(&me(MouseEventKind::Up(MouseButton::Left), 40, 40)),
Hit::Cancel
);
h.mouse(&me(MouseEventKind::Down(MouseButton::Left), 7, 6));
assert_eq!(
h.mouse(&me(MouseEventKind::Up(MouseButton::Left), 8, 7)),
Hit::Click
);
assert_eq!(h.mouse(&me(MouseEventKind::Moved, 0, 0)), Hit::HoverChanged);
assert!(!h.hover);
}
#[test]
fn outcome_combines_upwards() {
assert_eq!(Outcome::Ignored | Outcome::Consumed, Outcome::Consumed);
assert_eq!(Outcome::Changed | Outcome::Consumed, Outcome::Changed);
let mut o = Outcome::Ignored;
o |= Outcome::Changed;
assert!(o.is_changed());
}
#[test]
fn focus_wraps_and_survives_reorder() {
let mut f = Focus::new(['a', 'b', 'c']);
f.prev();
assert!(f.is('c'));
f.set_order(['c', 'd']);
assert!(f.is('c'));
f.wrap = false;
f.next();
f.next();
assert!(f.is('d'));
}
#[test]
fn word_boundary_crosses_punctuation_and_stops_at_the_ends() {
let g = ["o", "n", "e", ",", " ", "t", "w", "o"];
assert_eq!(word_boundary(&g, 0, true), 3);
assert_eq!(word_boundary(&g, 3, true), 8);
assert_eq!(word_boundary(&g, 8, false), 5);
assert_eq!(word_boundary(&g, 5, false), 0);
assert_eq!(word_boundary(&g, 8, true), 8);
assert_eq!(word_boundary(&g, 0, false), 0);
assert_eq!(word_boundary(&[], 0, true), 0);
}
}