zte/src/ui/input.rs

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
};
}
}