zte/src/ui/input.rs

337 lines
12 KiB
Rust

use super::*;
use crate::{
state::{Buffer, CursorId},
terminal::CursorStyle,
};
#[derive(Copy, Clone, Default)]
enum Mode {
#[default]
Doc,
Prompt,
Filter,
}
#[derive(Clone, Default)]
pub struct Input {
pub mode: Mode,
// 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 last_area: Area,
}
impl Input {
pub fn prompt() -> Self {
Self {
mode: Mode::Prompt,
..Self::default()
}
}
pub fn filter() -> Self {
Self {
mode: Mode::Filter,
..Self::default()
}
}
pub fn focus(&mut self, coord: [isize; 2]) {
for i in 0..2 {
self.focus[i] =
self.focus[i].clamp(coord[i] - self.last_area.size()[i] as isize + 1, 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,
buffer: &mut Buffer,
cursor_id: CursorId,
event: Event,
) -> Result<Resp, Event> {
buffer.begin_action();
match event.to_action(|e| {
e.to_char()
.map(Action::Char)
.or_else(|| e.to_move())
.or_else(|| e.to_pan())
.or_else(|| e.to_select_token())
.or_else(|| e.to_select_all())
.or_else(|| e.to_indent())
.or_else(|| e.to_mouse(self.last_area))
.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)) => {
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::Pan(dir, dist)) => {
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],
};
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], 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)) => {
buffer.goto_cursor(
cursor_id,
[self.focus[0] + pos[0], self.focus[1] + pos[1]],
true,
);
Ok(Resp::handled(None))
}
Some(
Action::Mouse(MouseAction::Drag, pos, false)
| Action::Mouse(MouseAction::Click, pos, true),
) => {
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(cursor_id) {
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
} else {
Ok(Resp::handled(Some(Event::Bell)))
}
}
Some(Action::Cut) => {
if buffer.cut(cursor_id) {
self.refocus(buffer, cursor_id);
Ok(Resp::handled(None))
} else {
Ok(Resp::handled(Some(Event::Bell)))
}
}
Some(Action::Paste) => {
if buffer.paste(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,
search: Option<&Search>,
frame: &mut Rect,
) {
// Add frame
let mut frame = frame.with_border(
if frame.has_focus() {
&state.theme.focus_border
} else {
&state.theme.border
},
title.as_deref(),
);
let Some(cursor) = buffer.cursors.get(cursor_id) else {
return;
};
let cursor_coord = buffer.text.to_coord(cursor.pos);
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
let margin_w = match self.mode {
Mode::Prompt => 2,
Mode::Filter => 0,
Mode::Doc => line_num_w + 2,
};
self.last_area = frame.rect([margin_w, 0], [!0, !0]).area();
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(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([0, 0], ">"),
Mode::Doc => frame
.rect([0, i], [margin_w, 1])
.with_bg(state.theme.margin_bg)
.with_fg(state.theme.margin_line_num)
.fill(' ')
.text([1, 0], &format!("{:>line_num_w$}", line_num + 1)),
};
// 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 line_selected = (line_pos..line_pos + line.len()).contains(&cursor.pos);
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, c) = match line.get(coord as usize).copied() {
Some('\n') if selected => (state.theme.whitespace, '⮠'),
Some(c) => {
if let Some(fg) = buffer
.highlights
.as_ref()
.and_then(|hl| hl.get_at(pos?))
.map(|tok| state.theme.token_color(tok.kind))
{
(fg, c)
} else {
(state.theme.text, c)
}
}
None => (Color::Reset, ' '),
};
let bg = match search.map(|s| s.contains(pos?)) {
Some(Some(true)) => state.theme.select_bg,
Some(Some(false)) => state.theme.search_result_bg,
Some(None) if line_selected && frame.has_focus() => {
state.theme.line_select_bg
}
_ => {
if selected {
if frame.has_focus() {
state.theme.select_bg
} else {
state.theme.unfocus_select_bg
}
} else if line_selected && frame.has_focus() {
state.theme.line_select_bg
} else {
Color::Reset
}
}
};
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();
}
}
}