use std::io;
use crossterm::cursor::SetCursorStyle;
use ratatui::{
buffer::Buffer,
layout::{Position, Rect, Size},
};
use crate::config::Bindings;
#[derive(Default, Clone, Copy)]
enum CursorState {
#[default]
Inactive,
Movement,
Selection,
}
impl CursorState {
fn is_active(&self) -> bool {
!matches!(self, Self::Inactive)
}
pub fn is_selecting(&self) -> bool {
matches!(self, Self::Selection)
}
fn toggle_selection(&mut self) {
match self {
Self::Inactive => (),
Self::Movement => {
*self = Self::Selection;
}
Self::Selection => {
*self = Self::Movement;
}
}
}
fn set_active(&mut self) {
*self = Self::Movement;
}
}
#[derive(Default, Clone, Copy)]
pub enum CursorDirection {
#[default]
Down,
Up,
Left,
Right,
}
impl CursorDirection {
fn go_from(&self, x: u16, y: u16) -> Position {
let mut x = x;
let mut y = y;
match self {
CursorDirection::Down => {
y = y.saturating_add(1);
}
CursorDirection::Up => {
y = y.saturating_sub(1);
}
CursorDirection::Left => {
x = x.saturating_sub(1);
}
CursorDirection::Right => {
x = x.saturating_add(1);
}
}
Position::new(x, y)
}
}
#[derive(Default, Clone)]
pub struct Cursor {
state: CursorState,
cursor: Option<Position>,
origin: Option<Position>,
rect: Option<Rect>,
pub is_dragging: bool,
pub leave_bind: String,
pub enter_bind: String,
pub copy_bind: String,
}
impl Cursor {
pub fn new(binds: &Bindings) -> Self {
let reversed = binds.keybind_reversed();
let leave_bind = reversed.get("ResetMode").cloned().unwrap_or_default();
let enter_bind = reversed.get("Cursor").cloned().unwrap_or_default();
let copy_bind = reversed.get("CopyPaste").cloned().unwrap_or_default();
Self {
state: CursorState::default(),
cursor: None,
origin: None,
rect: None,
is_dragging: false,
leave_bind,
enter_bind,
copy_bind,
}
}
pub fn rect(&self) -> Option<Rect> {
self.rect
}
pub fn cursor(&self) -> Option<Position> {
self.cursor
}
pub fn is_active(&self) -> bool {
self.state.is_active()
}
pub fn is_selecting(&self) -> bool {
self.state.is_selecting()
}
pub fn reset(&mut self) {
self.state = CursorState::default();
self.cursor = None;
self.origin = None;
self.rect = None;
self.is_dragging = false;
}
pub fn toggle(&mut self, position: Position) {
if self.state.is_active() {
self.toggle_selection();
} else {
self.start_cursor(position);
}
}
fn start_cursor(&mut self, position: Position) {
let _ = crossterm::execute!(io::stdout(), SetCursorStyle::SteadyBlock);
self.state.set_active();
self.cursor = Some(position);
self.origin = Some(position);
self.rect = None;
}
fn toggle_selection(&mut self) {
if !self.state.is_active() {
return;
}
self.state.toggle_selection();
if self.state.is_selecting() {
self.origin = self.cursor;
} else {
self.clear_selection();
}
}
fn clear_selection(&mut self) {
self.rect = None;
}
pub fn move_cursor(&mut self, direction: CursorDirection, Size { width, height }: Size) {
let Some(Position { x, y }) = self.cursor else {
return;
};
let new_pos = direction
.go_from(x, y)
.clamp(Position::ORIGIN, Position::new(width, height));
self.cursor = Some(new_pos);
}
pub fn move_cursor_to(&mut self, position: Position) {
if !self.state.is_active() {
return;
}
self.cursor = Some(position);
}
pub fn move_origin_to(&mut self, position: Position) {
if !self.state.is_active() {
return;
}
self.origin = Some(position);
}
pub fn extend_selection(&mut self) {
if !self.state.is_selecting() {
return;
}
let start = self.origin.expect("Should be set");
let end = self.cursor.expect("Should be set");
let x = start.x.min(end.x);
let y = start.y.min(end.y);
let width = u16::abs_diff(start.x, end.x) + 1;
let height = u16::abs_diff(start.y, end.y) + 1;
self.rect = Some(Rect::new(x, y, width, height));
}
pub fn mouse_drag(&mut self, row: u16, col: u16) {
let pos = Position::new(col, row);
self.move_cursor_to(pos);
if self.is_dragging {
self.extend_selection();
} else {
self.move_origin_to(pos);
self.is_dragging = true;
}
}
pub fn stop_drag(&mut self) {
self.is_dragging = false;
}
}
pub fn read_rect_from_buffer(rect: &Rect, buffer: &Buffer) -> String {
let mut content = String::new();
for y in rect.y..rect.y + rect.height {
for x in rect.x..rect.x + rect.width {
let Some(cell) = buffer.cell((x, y)) else {
continue;
};
content.push_str(cell.symbol());
}
content.push('\n')
}
content
}