413 lines
15 KiB
Rust
413 lines
15 KiB
Rust
use super::*;
|
|
use crate::{
|
|
state::{Buffer, Clipboard, CursorId},
|
|
terminal::CursorStyle,
|
|
};
|
|
|
|
#[derive(Copy, Clone, Default)]
|
|
enum Mode {
|
|
#[default]
|
|
Doc,
|
|
Prompt,
|
|
Filter,
|
|
SearchResult,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct Input {
|
|
pub mode: Mode,
|
|
line_offset: usize,
|
|
// x/y location in the buffer that the pane is trying to focus on
|
|
pub focus: [isize; 2],
|
|
// Remember the last area for things like scrolling
|
|
pub frame_area: Area,
|
|
pub last_area: Area,
|
|
pub last_scroll_pos: Option<([isize; 2], usize, usize)>,
|
|
pub scroll_grab: Option<(usize, isize)>,
|
|
}
|
|
|
|
impl Input {
|
|
pub fn prompt() -> Self {
|
|
Self {
|
|
mode: Mode::Prompt,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub fn filter() -> Self {
|
|
Self {
|
|
mode: Mode::Filter,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub fn search_result(line_offset: usize) -> Self {
|
|
Self {
|
|
mode: Mode::SearchResult,
|
|
line_offset,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub fn focus(&mut self, coord: [isize; 2]) {
|
|
for i in 0..2 {
|
|
self.focus[i] = self.focus[i]
|
|
.max(coord[i] - self.last_area.size()[i] as isize + 1)
|
|
.min(coord[i]);
|
|
}
|
|
}
|
|
|
|
pub fn refocus(&mut self, buffer: &mut Buffer, cursor_id: CursorId) {
|
|
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
|
return;
|
|
};
|
|
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
|
self.focus(cursor_coord);
|
|
}
|
|
|
|
pub fn handle(
|
|
&mut self,
|
|
clipboard: &mut Clipboard,
|
|
buffer: &mut Buffer,
|
|
cursor_id: CursorId,
|
|
event: Event,
|
|
) -> Result<Resp, Event> {
|
|
buffer.begin_action();
|
|
let is_doc = matches!(self.mode, Mode::Doc);
|
|
match event.to_action(|e| {
|
|
e.to_char()
|
|
.map(Action::Char)
|
|
.or_else(|| e.to_move())
|
|
.or_else(|| e.to_select_token())
|
|
.or_else(|| e.to_select_all())
|
|
.or_else(|| e.to_indent())
|
|
.or_else(|| e.to_edit())
|
|
}) {
|
|
Some(Action::Char(c)) => {
|
|
if c == '\x08' {
|
|
buffer.backspace(cursor_id);
|
|
} else if c == '\x7F' {
|
|
buffer.delete(cursor_id);
|
|
} else if c == '\n' {
|
|
buffer.newline(cursor_id);
|
|
} else {
|
|
buffer.enter(cursor_id, [c]);
|
|
}
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Move(dir, dist, retain_base, word))
|
|
if matches!(dir, Dir::Left | Dir::Right) || is_doc =>
|
|
{
|
|
let dist = match dist {
|
|
Dist::Char => [1, 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
|
|
Dist::Doc => [1_000_000_000; 2],
|
|
};
|
|
buffer.move_cursor(cursor_id, dir, dist, retain_base, word);
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Mouse(MouseAction::Scroll(dir), pos, _, _))
|
|
if is_doc && self.last_area.contains(pos).is_some() =>
|
|
{
|
|
let dist = [1, 1];
|
|
let dfocus = match dir {
|
|
Dir::Up => [0, -1],
|
|
Dir::Down => [0, 1],
|
|
Dir::Left => [-1, 0],
|
|
Dir::Right => [1, 0],
|
|
};
|
|
self.focus[0] += dfocus[0] * dist[0] as isize;
|
|
self.focus[1] += dfocus[1] * dist[1] as isize;
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Indent(forward)) => {
|
|
buffer.indent(cursor_id, forward);
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::GotoLine(line)) => {
|
|
buffer.goto_cursor(
|
|
cursor_id,
|
|
[0, (line - self.line_offset as isize).max(0)],
|
|
true,
|
|
);
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::SelectToken) => {
|
|
buffer.select_token_cursor(cursor_id);
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::SelectAll) => {
|
|
buffer.select_all_cursor(cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Mouse(MouseAction::Click, pos, false, drag_id)) => {
|
|
if let Some((scroll_pos, h, _)) = self.last_scroll_pos
|
|
&& let Some(pos) = self.frame_area.contains(pos)
|
|
&& scroll_pos[0] == pos[0]
|
|
&& (scroll_pos[1]..=scroll_pos[1] + h as isize).contains(&pos[1])
|
|
{
|
|
self.scroll_grab = Some((drag_id, pos[1] - scroll_pos[1]));
|
|
} else if let Some(pos) = self.last_area.contains(pos) {
|
|
let pos = [self.focus[0] + pos[0], self.focus[1] + pos[1]];
|
|
// If we're already in the right place, select the token instead
|
|
if let Some(cursor) = buffer.cursors.get(cursor_id)
|
|
&& cursor.selection().is_none()
|
|
&& buffer.text.to_coord(cursor.pos) == pos
|
|
{
|
|
buffer.select_token_cursor(cursor_id);
|
|
} else {
|
|
buffer.goto_cursor(cursor_id, pos, true);
|
|
}
|
|
}
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Mouse(MouseAction::Drag, pos, false, drag_id))
|
|
if self.scroll_grab.map_or(false, |(di, _)| di == drag_id) =>
|
|
{
|
|
if let Some((_, offset)) = self.scroll_grab
|
|
&& let Some((_, scroll_sz, frame_sz)) = self.last_scroll_pos
|
|
{
|
|
self.focus[1] = ((self.frame_area.translate(pos)[1] - offset).max(0) as usize
|
|
* buffer.text.lines().count()
|
|
/ frame_sz) as isize;
|
|
}
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(
|
|
Action::Mouse(MouseAction::Drag, pos, false, _)
|
|
| Action::Mouse(MouseAction::Click, pos, true, _),
|
|
) => {
|
|
let pos = self.last_area.translate(pos);
|
|
buffer.goto_cursor(
|
|
cursor_id,
|
|
[self.focus[0] + pos[0], self.focus[1] + pos[1]],
|
|
false,
|
|
);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Undo) => {
|
|
if buffer.undo() {
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
} else {
|
|
Ok(Resp::handled(Some(Event::Bell)))
|
|
}
|
|
}
|
|
Some(Action::Redo) => {
|
|
if buffer.redo() {
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
} else {
|
|
Ok(Resp::handled(Some(Event::Bell)))
|
|
}
|
|
}
|
|
Some(Action::Copy) => {
|
|
if buffer.copy(clipboard, cursor_id) {
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
} else {
|
|
Ok(Resp::handled(Some(Event::Bell)))
|
|
}
|
|
}
|
|
Some(Action::Cut) => {
|
|
if buffer.cut(clipboard, cursor_id) {
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
} else {
|
|
Ok(Resp::handled(Some(Event::Bell)))
|
|
}
|
|
}
|
|
Some(Action::Paste) => {
|
|
if buffer.paste(clipboard, cursor_id) {
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
} else {
|
|
Ok(Resp::handled(Some(Event::Bell)))
|
|
}
|
|
}
|
|
Some(Action::Duplicate) => {
|
|
buffer.duplicate(cursor_id);
|
|
self.refocus(buffer, cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
Some(Action::Comment) => {
|
|
buffer.comment(cursor_id);
|
|
Ok(Resp::handled(None))
|
|
}
|
|
_ => Err(event),
|
|
}
|
|
}
|
|
|
|
pub fn render(
|
|
&mut self,
|
|
state: &State,
|
|
title: Option<&str>,
|
|
buffer: &Buffer,
|
|
cursor_id: CursorId,
|
|
finder: Option<&Finder>,
|
|
outer_frame: &mut Rect,
|
|
) {
|
|
self.frame_area = outer_frame.area();
|
|
|
|
// Add frame
|
|
let mut frame = if matches!(self.mode, Mode::SearchResult) {
|
|
outer_frame.rect([0; 2], [!0; 2])
|
|
} else {
|
|
outer_frame.with_border(
|
|
if outer_frame.has_focus() {
|
|
&state.theme.focus_border
|
|
} else {
|
|
&state.theme.border
|
|
},
|
|
title.as_deref(),
|
|
)
|
|
};
|
|
|
|
let (line_num_w, margin_w) = match self.mode {
|
|
Mode::Prompt => (2, 0),
|
|
Mode::Filter => (0, 0),
|
|
Mode::Doc => {
|
|
let line_num_w = (self.line_offset + buffer.text.lines().count())
|
|
.max(1)
|
|
.ilog10() as usize
|
|
+ 1;
|
|
(line_num_w, line_num_w + 2)
|
|
}
|
|
Mode::SearchResult => (4, 6),
|
|
};
|
|
|
|
self.last_area = frame.rect([margin_w, 0], [!0, !0]).area();
|
|
|
|
let Some(cursor) = buffer.cursors.get(cursor_id) else {
|
|
return;
|
|
};
|
|
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
|
|
|
let mut pos = 0;
|
|
for (i, (line_num, (line_pos, line))) in buffer
|
|
.text
|
|
.lines()
|
|
.map(move |line| {
|
|
let line_pos = pos;
|
|
pos += line.len();
|
|
(line_pos, line)
|
|
})
|
|
.enumerate()
|
|
.skip(self.focus[1].max(0) as usize)
|
|
.enumerate()
|
|
.take(frame.size()[1])
|
|
{
|
|
// Margin
|
|
match self.mode {
|
|
Mode::Filter => frame.rect([0, 0], frame.size()),
|
|
Mode::Prompt => frame
|
|
.rect([0, i], [1, 1])
|
|
.with_bg_preference(state.theme.margin_bg)
|
|
.with_fg(state.theme.margin_line_num)
|
|
.fill(' ')
|
|
.text([0, 0], ">"),
|
|
Mode::Doc | Mode::SearchResult => frame
|
|
.rect([0, i], [margin_w, 1])
|
|
.with_bg_preference(state.theme.margin_bg)
|
|
.with_fg(state.theme.margin_line_num)
|
|
.fill(' ')
|
|
.text(
|
|
[1, 0],
|
|
&format!("{:>line_num_w$}", self.line_offset + line_num + 1),
|
|
),
|
|
};
|
|
|
|
let line_highlight_selected = matches!(self.mode, Mode::Doc)
|
|
&& buffer.text.to_coord(cursor.pos)[1] == line_num as isize;
|
|
|
|
// Line
|
|
{
|
|
let mut frame = frame.rect([margin_w, i], [!0, 1]);
|
|
for i in 0..frame.size()[0] {
|
|
let coord = self.focus[0] + i as isize;
|
|
let pos = if i < line.len() {
|
|
Some(line_pos + coord as usize)
|
|
} else {
|
|
None
|
|
};
|
|
let selected = cursor
|
|
.selection()
|
|
.zip(pos)
|
|
.map_or(false, |(s, pos)| s.contains(&pos));
|
|
let (fg, hl_bg, c) = match line.get(coord as usize).copied() {
|
|
Some('\n') if selected => (state.theme.whitespace, None, '⮠'),
|
|
Some(c) => {
|
|
if let Some((fg, bg)) = pos
|
|
.and_then(|pos| buffer.highlights.get_at(pos))
|
|
.map(|tok| state.theme.token_color(tok.kind))
|
|
{
|
|
(fg, bg, c)
|
|
} else {
|
|
(state.theme.text, None, c)
|
|
}
|
|
}
|
|
None => (Color::Reset, None, ' '),
|
|
};
|
|
let bg = match finder.map(|s| s.contains(pos?)) {
|
|
Some(Some(true)) => state.theme.select_bg,
|
|
Some(Some(false)) => state.theme.search_result_bg,
|
|
_ => {
|
|
if selected {
|
|
if frame.has_focus() {
|
|
state.theme.select_bg
|
|
} else {
|
|
state.theme.unfocus_select_bg
|
|
}
|
|
} else if let Some(hl_bg) = hl_bg {
|
|
hl_bg
|
|
} else if line_highlight_selected && frame.has_focus() {
|
|
state.theme.line_select_bg
|
|
} else {
|
|
frame.bg
|
|
}
|
|
}
|
|
};
|
|
frame
|
|
.with_bg(bg)
|
|
.with_fg(fg)
|
|
.text([i as isize, 0], c.encode_utf8(&mut [0; 4]));
|
|
}
|
|
|
|
// Set cursor position
|
|
if cursor_coord[1] == line_num as isize {
|
|
frame.set_cursor(
|
|
[cursor_coord[0] - self.focus[0], 0],
|
|
CursorStyle::BlinkingBar,
|
|
);
|
|
}
|
|
}
|
|
|
|
pos += line.len();
|
|
}
|
|
|
|
// TODO: Clean this up
|
|
let line_count = buffer.text.lines().count();
|
|
let frame_sz = outer_frame.size()[1].saturating_sub(2).max(1);
|
|
let scroll_sz = (frame_sz * frame_sz / line_count).max(1).min(frame_sz);
|
|
self.last_scroll_pos = if scroll_sz != frame_sz {
|
|
let lines2 = line_count.saturating_sub(frame_sz).max(1);
|
|
let offset = frame_sz.saturating_sub(scroll_sz)
|
|
* (self.focus[1].max(0) as usize).min(lines2)
|
|
/ lines2;
|
|
let pos = [outer_frame.size()[0].saturating_sub(1), 1 + offset];
|
|
outer_frame
|
|
.rect(pos, [1, scroll_sz])
|
|
.with_bg(Color::White)
|
|
.fill(' ');
|
|
Some((pos.map(|e| e as isize), scroll_sz, frame_sz))
|
|
} else {
|
|
None
|
|
};
|
|
}
|
|
}
|