Added basic mouse inputs

This commit is contained in:
Joshua Barretto 2025-09-22 15:28:17 +01:00
parent 85c88402ca
commit 5925e37fba
6 changed files with 183 additions and 72 deletions

View file

@ -1,5 +1,10 @@
use crate::{state::BufferId, terminal::TerminalEvent}; use crate::{
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; state::BufferId,
terminal::{Area, TerminalEvent},
};
use crossterm::event::{
KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind,
};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -14,7 +19,7 @@ pub enum Dir {
pub enum Action { pub enum Action {
Char(char), // Insert a character Char(char), // Insert a character
Indent(bool), // Indent (indent vs deindent) Indent(bool), // Indent (indent vs deindent)
Move(Dir, Dist, bool, bool), // Move the cursor (dir, page, retain_base, word) Move(Dir, Dist, bool, bool), // Move the cursor (dir, dist, retain_base, word)
PaneMove(Dir), // Move panes PaneMove(Dir), // Move panes
PaneOpen(Dir), // Create a new pane PaneOpen(Dir), // Create a new pane
PaneClose, // Close the current pane PaneClose, // Close the current pane
@ -36,6 +41,7 @@ pub enum Action {
SelectToken, // Fully select the token under the cursor SelectToken, // Fully select the token under the cursor
SelectAll, // Fully select the entire input SelectAll, // Fully select the entire input
Save, // Save the current buffer Save, // Save the current buffer
Mouse(MouseAction, [isize; 2]),
} }
/// How far should movement go? /// How far should movement go?
@ -46,6 +52,13 @@ pub enum Dist {
Doc, Doc,
} }
#[derive(Clone, Debug)]
pub enum MouseAction {
Click,
ScrollDown,
ScrollUp,
}
#[derive(Debug)] #[derive(Debug)]
pub enum Event { pub enum Event {
// The incoming event is an action generated by some other internal component. // The incoming event is an action generated by some other internal component.
@ -157,28 +170,33 @@ impl RawEvent {
} }
pub fn to_move(&self) -> Option<Action> { pub fn to_move(&self) -> Option<Action> {
let TerminalEvent::Key(KeyEvent { let (dir, dist, retain_base, word) = match &self.0 {
code, TerminalEvent::Mouse(ev) => match ev.kind {
modifiers, MouseEventKind::ScrollUp => (Dir::Up, Dist::Char, false, false),
kind: KeyEventKind::Press | KeyEventKind::Repeat, MouseEventKind::ScrollDown => (Dir::Down, Dist::Char, false, false),
.. _ => return None,
}) = &self.0 },
else { TerminalEvent::Key(KeyEvent {
return None; code,
}; modifiers,
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
}) => {
let retain_base = modifiers.contains(KeyModifiers::SHIFT);
let word = modifiers.contains(KeyModifiers::CONTROL);
let retain_base = modifiers.contains(KeyModifiers::SHIFT); match code {
let word = modifiers.contains(KeyModifiers::CONTROL); KeyCode::Home => (Dir::Up, Dist::Doc, retain_base, word),
KeyCode::End => (Dir::Down, Dist::Doc, retain_base, word),
let (dir, dist) = match code { KeyCode::PageUp => (Dir::Up, Dist::Page, retain_base, word),
KeyCode::Home => (Dir::Up, Dist::Doc), KeyCode::PageDown => (Dir::Down, Dist::Page, retain_base, word),
KeyCode::End => (Dir::Down, Dist::Doc), KeyCode::Left => (Dir::Left, Dist::Char, retain_base, word),
KeyCode::PageUp => (Dir::Up, Dist::Page), KeyCode::Right => (Dir::Right, Dist::Char, retain_base, word),
KeyCode::PageDown => (Dir::Down, Dist::Page), KeyCode::Up => (Dir::Up, Dist::Char, retain_base, word),
KeyCode::Left => (Dir::Left, Dist::Char), KeyCode::Down => (Dir::Down, Dist::Char, retain_base, word),
KeyCode::Right => (Dir::Right, Dist::Char), _ => return None,
KeyCode::Up => (Dir::Up, Dist::Char), }
KeyCode::Down => (Dir::Down, Dist::Char), }
_ => return None, _ => return None,
}; };
@ -390,4 +408,23 @@ impl RawEvent {
None None
} }
} }
pub fn to_mouse(&self, area: Area) -> Option<Action> {
let TerminalEvent::Mouse(ev) = self.0 else {
return None;
};
if let Some(pos) = area.contains([ev.column as isize, ev.row as isize]) {
match ev.kind {
MouseEventKind::ScrollUp => Some(Action::Mouse(MouseAction::ScrollUp, pos)),
MouseEventKind::ScrollDown => Some(Action::Mouse(MouseAction::ScrollDown, pos)),
MouseEventKind::Down(MouseButton::Left) => {
Some(Action::Mouse(MouseAction::Click, pos))
}
_ => None,
}
} else {
None
}
}
} }

View file

@ -6,9 +6,9 @@ mod theme;
mod ui; mod ui;
use crate::{ use crate::{
action::{Action, Dir, Dist, Event}, action::{Action, Dir, Dist, Event, MouseAction},
state::State, state::State,
terminal::{Color, Terminal, TerminalEvent}, terminal::{Area, Color, Terminal, TerminalEvent},
ui::{Element as _, Visual as _}, ui::{Element as _, Visual as _},
}; };
use clap::Parser; use clap::Parser;

View file

@ -216,11 +216,11 @@ impl Buffer {
}); });
} }
pub fn goto_line_cursor(&mut self, cursor_id: CursorId, line: isize) { pub fn goto_cursor(&mut self, cursor_id: CursorId, pos: [isize; 2]) {
let Some(cursor) = self.cursors.get_mut(cursor_id) else { let Some(cursor) = self.cursors.get_mut(cursor_id) else {
return; return;
}; };
cursor.pos = self.text.to_pos([0, line]); cursor.pos = self.text.to_pos(pos);
cursor.reset_desired_col(&self.text); cursor.reset_desired_col(&self.text);
cursor.base = cursor.pos; cursor.base = cursor.pos;
} }

View file

@ -31,11 +31,38 @@ impl Default for Cell {
} }
} }
/// Represents an area of the terminal window
#[derive(Copy, Clone, Default)]
pub struct Area {
origin: [u16; 2],
size: [u16; 2],
}
impl Area {
pub fn size(&self) -> [usize; 2] {
self.size.map(|e| e as usize)
}
pub fn contains(&self, pos: [isize; 2]) -> Option<[isize; 2]> {
if (self.origin[0] as isize..self.origin[0] as isize + self.size[0] as isize)
.contains(&pos[0])
&& (self.origin[1] as isize..self.origin[1] as isize + self.size[1] as isize)
.contains(&pos[1])
{
Some([
pos[0] - self.origin[0] as isize,
pos[1] - self.origin[1] as isize,
])
} else {
None
}
}
}
pub struct Rect<'a> { pub struct Rect<'a> {
fg: Color, fg: Color,
bg: Color, bg: Color,
origin: [u16; 2], area: Area,
size: [u16; 2],
fb: &'a mut Framebuffer, fb: &'a mut Framebuffer,
has_focus: bool, has_focus: bool,
} }
@ -44,8 +71,8 @@ impl<'a> Rect<'a> {
fn get_mut(&mut self, pos: [usize; 2]) -> Option<&mut Cell> { fn get_mut(&mut self, pos: [usize; 2]) -> Option<&mut Cell> {
if pos[0] < self.size()[0] && pos[1] < self.size()[1] { if pos[0] < self.size()[0] && pos[1] < self.size()[1] {
let offs = [ let offs = [
self.origin[0] as usize + pos[0], self.area.origin[0] as usize + pos[0],
self.origin[1] as usize + pos[1], self.area.origin[1] as usize + pos[1],
]; ];
Some(&mut self.fb.cells[offs[1] * self.fb.size[0] as usize + offs[0]]) Some(&mut self.fb.cells[offs[1] * self.fb.size[0] as usize + offs[0]])
} else { } else {
@ -59,14 +86,16 @@ impl<'a> Rect<'a> {
pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect { pub fn rect(&mut self, origin: [usize; 2], size: [usize; 2]) -> Rect {
Rect { Rect {
origin: [ area: Area {
self.origin[0] + origin[0] as u16, origin: [
self.origin[1] + origin[1] as u16, self.area.origin[0] + origin[0] as u16,
], self.area.origin[1] + origin[1] as u16,
size: [ ],
size[0].min((self.size[0] as usize).saturating_sub(origin[0])) as u16, size: [
size[1].min((self.size[1] as usize).saturating_sub(origin[1])) as u16, size[0].min((self.area.size[0] as usize).saturating_sub(origin[0])) as u16,
], size[1].min((self.area.size[1] as usize).saturating_sub(origin[1])) as u16,
],
},
fg: self.fg, fg: self.fg,
bg: self.bg, bg: self.bg,
fb: self.fb, fb: self.fb,
@ -132,8 +161,7 @@ impl<'a> Rect<'a> {
Rect { Rect {
fg, fg,
bg: self.bg, bg: self.bg,
origin: self.origin, area: self.area,
size: self.size,
fb: self.fb, fb: self.fb,
has_focus: self.has_focus, has_focus: self.has_focus,
} }
@ -143,8 +171,7 @@ impl<'a> Rect<'a> {
Rect { Rect {
fg: self.fg, fg: self.fg,
bg, bg,
origin: self.origin, area: self.area,
size: self.size,
fb: self.fb, fb: self.fb,
has_focus: self.has_focus, has_focus: self.has_focus,
} }
@ -154,8 +181,7 @@ impl<'a> Rect<'a> {
Rect { Rect {
fg: self.fg, fg: self.fg,
bg: self.bg, bg: self.bg,
origin: self.origin, area: self.area,
size: self.size,
fb: self.fb, fb: self.fb,
has_focus: self.has_focus && focus, has_focus: self.has_focus && focus,
} }
@ -165,8 +191,12 @@ impl<'a> Rect<'a> {
self.has_focus self.has_focus
} }
pub fn area(&self) -> Area {
self.area
}
pub fn size(&self) -> [usize; 2] { pub fn size(&self) -> [usize; 2] {
self.size.map(|e| e as usize) self.area.size.map(|e| e as usize)
} }
pub fn fill(&mut self, c: char) -> Rect { pub fn fill(&mut self, c: char) -> Rect {
@ -211,8 +241,8 @@ impl<'a> Rect<'a> {
{ {
self.fb.cursor = Some(( self.fb.cursor = Some((
[ [
self.origin[0] + cursor[0] as u16, self.area.origin[0] + cursor[0] as u16,
self.origin[1] + cursor[1] as u16, self.area.origin[1] + cursor[1] as u16,
], ],
style, style,
)); ));
@ -233,8 +263,10 @@ impl Framebuffer {
Rect { Rect {
fg: Color::Reset, fg: Color::Reset,
bg: Color::Reset, bg: Color::Reset,
origin: [0, 0], area: Area {
size: self.size, origin: [0, 0],
size: self.size,
},
fb: self, fb: self,
has_focus: true, has_focus: true,
} }
@ -251,12 +283,14 @@ impl<'a> Terminal<'a> {
fn enter(mut stdout: impl io::Write) { fn enter(mut stdout: impl io::Write) {
let _ = terminal::enable_raw_mode(); let _ = terminal::enable_raw_mode();
let _ = stdout.execute(terminal::EnterAlternateScreen); let _ = stdout.execute(terminal::EnterAlternateScreen);
let _ = stdout.execute(event::EnableMouseCapture);
} }
fn leave(mut stdout: impl io::Write) { fn leave(mut stdout: impl io::Write) {
let _ = terminal::disable_raw_mode(); let _ = terminal::disable_raw_mode();
let _ = stdout.execute(terminal::LeaveAlternateScreen); let _ = stdout.execute(terminal::LeaveAlternateScreen);
let _ = stdout.execute(cursor::Show); let _ = stdout.execute(cursor::Show);
let _ = stdout.execute(event::DisableMouseCapture);
} }
pub fn with<T>( pub fn with<T>(

View file

@ -17,8 +17,8 @@ pub struct Input {
pub mode: Mode, pub mode: Mode,
// x/y location in the buffer that the pane is trying to focus on // x/y location in the buffer that the pane is trying to focus on
pub focus: [isize; 2], pub focus: [isize; 2],
// Remember the last known size for things like scrolling // Remember the last area for things like scrolling
pub last_size: [usize; 2], pub last_area: Area,
} }
impl Input { impl Input {
@ -39,7 +39,7 @@ impl Input {
pub fn focus(&mut self, coord: [isize; 2]) { pub fn focus(&mut self, coord: [isize; 2]) {
for i in 0..2 { for i in 0..2 {
self.focus[i] = self.focus[i] =
self.focus[i].clamp(coord[i] - self.last_size[i] as isize + 1, coord[i]); self.focus[i].clamp(coord[i] - self.last_area.size()[i] as isize + 1, coord[i]);
} }
} }
@ -64,6 +64,7 @@ impl Input {
.or_else(|| e.to_select_token()) .or_else(|| e.to_select_token())
.or_else(|| e.to_select_all()) .or_else(|| e.to_select_all())
.or_else(|| e.to_indent()) .or_else(|| e.to_indent())
.or_else(|| e.to_mouse(self.last_area))
}) { }) {
Some(Action::Char(c)) => { Some(Action::Char(c)) => {
if c == '\x08' { if c == '\x08' {
@ -81,7 +82,7 @@ impl Input {
Some(Action::Move(dir, dist, retain_base, word)) => { Some(Action::Move(dir, dist, retain_base, word)) => {
let dist = match dist { let dist = match dist {
Dist::Char => [1, 1], Dist::Char => [1, 1],
Dist::Page => self.last_size.map(|s| s.saturating_sub(3).max(1)), Dist::Page => self.last_area.size().map(|s| s.saturating_sub(3).max(1)),
// TODO: Don't just use an arbitrary very large number // TODO: Don't just use an arbitrary very large number
Dist::Doc => [1_000_000_000; 2], Dist::Doc => [1_000_000_000; 2],
}; };
@ -94,7 +95,7 @@ impl Input {
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::GotoLine(line)) => { Some(Action::GotoLine(line)) => {
buffer.goto_line_cursor(cursor_id, line); buffer.goto_cursor(cursor_id, [0, line]);
self.refocus(buffer, cursor_id); self.refocus(buffer, cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
@ -106,6 +107,11 @@ impl Input {
buffer.select_all_cursor(cursor_id); buffer.select_all_cursor(cursor_id);
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Mouse(MouseAction::Click, pos)) => {
buffer.goto_cursor(cursor_id, [self.focus[0] + pos[0], self.focus[1] + pos[1]]);
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
}
_ => Err(event), _ => Err(event),
} }
} }
@ -141,7 +147,7 @@ impl Input {
Mode::Doc => line_num_w + 2, Mode::Doc => line_num_w + 2,
}; };
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]]; self.last_area = frame.rect([margin_w, 0], [!0, !0]).area();
let mut pos = 0; let mut pos = 0;
for (i, (line_num, (line_pos, line))) in buffer for (i, (line_num, (line_pos, line))) in buffer

View file

@ -1,14 +1,20 @@
use super::*; use super::*;
use crate::state::BufferId; use crate::state::BufferId;
pub enum Pane { pub enum PaneKind {
Empty, Empty,
Doc(Doc), Doc(Doc),
} }
pub struct Pane {
kind: PaneKind,
last_area: Area,
}
pub struct Panes { pub struct Panes {
selected: usize, selected: usize,
panes: Vec<Pane>, panes: Vec<Pane>,
last_area: Area,
} }
impl Panes { impl Panes {
@ -17,8 +23,12 @@ impl Panes {
selected: 0, selected: 0,
panes: buffers panes: buffers
.iter() .iter()
.map(|b| Pane::Doc(Doc::new(state, *b))) .map(|b| Pane {
kind: PaneKind::Doc(Doc::new(state, *b)),
last_area: Area::default(),
})
.collect(), .collect(),
last_area: Default::default(),
} }
} }
@ -34,6 +44,7 @@ impl Element for Panes {
.map(Action::PaneMove) .map(Action::PaneMove)
.or_else(|| e.to_pane_open().map(Action::PaneOpen)) .or_else(|| e.to_pane_open().map(Action::PaneOpen))
.or_else(|| e.to_pane_close()) .or_else(|| e.to_pane_close())
.or_else(|| e.to_mouse(self.last_area))
}) { }) {
Some(Action::PaneMove(Dir::Left)) => { Some(Action::PaneMove(Dir::Left)) => {
self.selected = (self.selected + self.panes.len() - 1) % self.panes.len(); self.selected = (self.selected + self.panes.len() - 1) % self.panes.len();
@ -45,9 +56,9 @@ impl Element for Panes {
} }
Some(Action::PaneClose) => { Some(Action::PaneClose) => {
if self.selected < self.panes.len() { if self.selected < self.panes.len() {
match self.panes.remove(self.selected) { match self.panes.remove(self.selected).kind {
Pane::Empty => {} PaneKind::Empty => {}
Pane::Doc(doc) => doc.close(state), PaneKind::Doc(doc) => doc.close(state),
} }
self.selected = self.selected.clamp(0, self.panes.len().saturating_sub(1)); self.selected = self.selected.clamp(0, self.panes.len().saturating_sub(1));
Ok(Resp::handled(None)) Ok(Resp::handled(None))
@ -61,21 +72,39 @@ impl Element for Panes {
Dir::Right => (self.selected + 1).min(self.panes.len()), Dir::Right => (self.selected + 1).min(self.panes.len()),
Dir::Up | Dir::Down => return Err(event), Dir::Up | Dir::Down => return Err(event),
}; };
let pane = match state.buffers.keys().next() { let kind = match state.buffers.keys().next() {
Some(b) => Pane::Doc(Doc::new(state, b)), Some(b) => PaneKind::Doc(Doc::new(state, b)),
None => Pane::Empty, None => PaneKind::Empty,
}; };
self.panes.insert(new_idx, pane); self.panes.insert(
new_idx,
Pane {
kind,
last_area: Area::default(),
},
);
self.selected = new_idx; self.selected = new_idx;
Ok(Resp::handled(None)) Ok(Resp::handled(None))
} }
Some(Action::Mouse(_, pos)) => {
for (i, pane) in self.panes.iter_mut().enumerate() {
if pane.last_area.contains(pos).is_some() {
self.selected = i;
match &mut pane.kind {
PaneKind::Doc(doc) => return doc.handle(state, event),
PaneKind::Empty => {}
}
}
}
Ok(Resp::handled(None))
}
// Pass anything else through to the active pane // Pass anything else through to the active pane
_ => { _ => {
if let Some(pane) = self.panes.get_mut(self.selected) { if let Some(pane) = self.panes.get_mut(self.selected) {
// Pass to pane // Pass to pane
match pane { match &mut pane.kind {
Pane::Empty => Err(event), PaneKind::Empty => Err(event),
Pane::Doc(doc) => doc.handle(state, event), PaneKind::Doc(doc) => doc.handle(state, event),
} }
} else { } else {
// No active pane, don't handle // No active pane, don't handle
@ -92,6 +121,8 @@ impl Visual for Panes {
let frame_w = frame.size()[0]; let frame_w = frame.size()[0];
let boundary = |i| frame_w * i / n; let boundary = |i| frame_w * i / n;
self.last_area = frame.area();
for (i, pane) in self.panes.iter_mut().enumerate() { for (i, pane) in self.panes.iter_mut().enumerate() {
let (x0, x1) = (boundary(i), boundary(i + 1)); let (x0, x1) = (boundary(i), boundary(i + 1));
@ -99,9 +130,12 @@ impl Visual for Panes {
frame frame
.rect([x0, 0], [x1 - x0, frame.size()[1]]) .rect([x0, 0], [x1 - x0, frame.size()[1]])
.with_focus(self.selected == i) .with_focus(self.selected == i)
.with(|frame| match pane { .with(|frame| {
Pane::Empty => {} pane.last_area = frame.area();
Pane::Doc(doc) => doc.render(state, frame), match &mut pane.kind {
PaneKind::Empty => {}
PaneKind::Doc(doc) => doc.render(state, frame),
}
}); });
} }
} }