Added page scrolling and cursor selection
This commit is contained in:
parent
d96fc47476
commit
81ab27cbbf
9 changed files with 251 additions and 104 deletions
|
|
@ -13,7 +13,7 @@ pub enum Dir {
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
Char(char), // Insert a character
|
Char(char), // Insert a character
|
||||||
Backspace, // Backspace a character
|
Backspace, // Backspace a character
|
||||||
Move(Dir), // Move the cursor
|
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base)
|
||||||
PaneMove(Dir), // Move panes
|
PaneMove(Dir), // Move panes
|
||||||
Cancel, // Cancels the current context
|
Cancel, // Cancels the current context
|
||||||
Go, // Search, accept, or select the current option
|
Go, // Search, accept, or select the current option
|
||||||
|
|
@ -92,22 +92,34 @@ impl RawEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_move(&self) -> Option<Dir> {
|
pub fn to_move(&self) -> Option<Action> {
|
||||||
match &self.0 {
|
let TerminalEvent::Key(KeyEvent {
|
||||||
TerminalEvent::Key(KeyEvent {
|
code,
|
||||||
code,
|
modifiers,
|
||||||
modifiers: KeyModifiers::NONE,
|
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
||||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
..
|
||||||
..
|
}) = &self.0
|
||||||
}) => match code {
|
else {
|
||||||
KeyCode::Left => Some(Dir::Left),
|
return None;
|
||||||
KeyCode::Right => Some(Dir::Right),
|
};
|
||||||
KeyCode::Up => Some(Dir::Up),
|
|
||||||
KeyCode::Down => Some(Dir::Down),
|
let retain_base = match *modifiers {
|
||||||
_ => None,
|
KeyModifiers::NONE => false,
|
||||||
},
|
KeyModifiers::SHIFT => true,
|
||||||
_ => None,
|
_ => return None,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
let (dir, page) = match code {
|
||||||
|
KeyCode::PageUp => (Dir::Up, true),
|
||||||
|
KeyCode::PageDown => (Dir::Down, true),
|
||||||
|
KeyCode::Left => (Dir::Left, false),
|
||||||
|
KeyCode::Right => (Dir::Right, false),
|
||||||
|
KeyCode::Up => (Dir::Up, false),
|
||||||
|
KeyCode::Down => (Dir::Down, false),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Action::Move(dir, page, retain_base))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_open_prompt(&self) -> Option<Action> {
|
pub fn to_open_prompt(&self) -> Option<Action> {
|
||||||
|
|
|
||||||
154
src/state.rs
154
src/state.rs
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
use slotmap::{HopSlotMap, new_key_type};
|
use slotmap::{HopSlotMap, new_key_type};
|
||||||
use std::{io, path::PathBuf};
|
use std::{io, ops::Range, path::PathBuf};
|
||||||
|
|
||||||
new_key_type! {
|
new_key_type! {
|
||||||
pub struct BufferId;
|
pub struct BufferId;
|
||||||
|
|
@ -13,6 +13,7 @@ new_key_type! {
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
#[derive(Copy, Clone, Default)]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
|
pub base: usize,
|
||||||
pub pos: usize,
|
pub pos: usize,
|
||||||
// Used to 'remember' the desired column when skipping over shorter lines
|
// Used to 'remember' the desired column when skipping over shorter lines
|
||||||
desired_col: isize,
|
desired_col: isize,
|
||||||
|
|
@ -22,6 +23,14 @@ impl Cursor {
|
||||||
fn reset_desired_col(&mut self, text: &Text) {
|
fn reset_desired_col(&mut self, text: &Text) {
|
||||||
self.desired_col = text.to_coord(self.pos)[0];
|
self.desired_col = text.to_coord(self.pos)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selection(&self) -> Option<Range<usize>> {
|
||||||
|
if self.base == self.pos {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.base.min(self.pos)..self.base.max(self.pos))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Text {
|
pub struct Text {
|
||||||
|
|
@ -31,16 +40,17 @@ pub struct Text {
|
||||||
impl Text {
|
impl Text {
|
||||||
pub fn to_coord(&self, pos: usize) -> [isize; 2] {
|
pub fn to_coord(&self, pos: usize) -> [isize; 2] {
|
||||||
let mut n = 0;
|
let mut n = 0;
|
||||||
let mut i = 0;
|
let mut last_n = 0;
|
||||||
|
let mut i: usize = 0;
|
||||||
for line in self.lines() {
|
for line in self.lines() {
|
||||||
if (n..n + line.len() + 1).contains(&pos) {
|
last_n = n;
|
||||||
return [(pos - n) as isize, i as isize];
|
i += 1;
|
||||||
} else {
|
if (n..n + line.len()).contains(&pos) {
|
||||||
n += line.len() + 1;
|
break;
|
||||||
i += 1;
|
|
||||||
}
|
}
|
||||||
|
n += line.len();
|
||||||
}
|
}
|
||||||
[0, i as isize]
|
[(pos - last_n) as isize, i.saturating_sub(1) as isize]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_pos(&self, mut coord: [isize; 2]) -> usize {
|
pub fn to_pos(&self, mut coord: [isize; 2]) -> usize {
|
||||||
|
|
@ -50,16 +60,41 @@ impl Text {
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
for (i, line) in self.lines().enumerate() {
|
for (i, line) in self.lines().enumerate() {
|
||||||
if i as isize == coord[1] {
|
if i as isize == coord[1] {
|
||||||
return pos + coord[0].clamp(0, line.len() as isize) as usize;
|
return pos + coord[0].clamp(0, line.len().saturating_sub(1) as isize) as usize;
|
||||||
} else {
|
} else {
|
||||||
pos += line.len() + 1;
|
pos += line.len();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos.min(self.chars.len())
|
pos.min(self.chars.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an iterator over the lines of the text.
|
||||||
|
///
|
||||||
|
/// Guarantees:
|
||||||
|
/// - If you sum the lengths of each line, it will be the same as the length (in characters) of the text
|
||||||
pub fn lines(&self) -> impl Iterator<Item = &[char]> {
|
pub fn lines(&self) -> impl Iterator<Item = &[char]> {
|
||||||
self.chars.split(|c| *c == '\n')
|
let mut start = 0;
|
||||||
|
let mut i = 0;
|
||||||
|
let mut finished = false;
|
||||||
|
core::iter::from_fn(move || {
|
||||||
|
loop {
|
||||||
|
let Some(c) = self.chars.get(i) else {
|
||||||
|
return if finished {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let line = &self.chars[start..];
|
||||||
|
finished = true;
|
||||||
|
Some(line)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
if *c == '\n' {
|
||||||
|
let line = &self.chars[start..i];
|
||||||
|
start = i;
|
||||||
|
return Some(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,17 +119,23 @@ impl Buffer {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor(&mut self, cursor_id: CursorId, dir: Dir) {
|
pub fn move_cursor(
|
||||||
|
&mut self,
|
||||||
|
cursor_id: CursorId,
|
||||||
|
dir: Dir,
|
||||||
|
dist: [usize; 2],
|
||||||
|
retain_base: bool,
|
||||||
|
) {
|
||||||
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
let Some(cursor) = self.cursors.get_mut(cursor_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
match dir {
|
match dir {
|
||||||
Dir::Left => {
|
Dir::Left => {
|
||||||
cursor.pos = cursor.pos.saturating_sub(1);
|
cursor.pos = cursor.pos.saturating_sub(dist[0]);
|
||||||
cursor.reset_desired_col(&self.text);
|
cursor.reset_desired_col(&self.text);
|
||||||
}
|
}
|
||||||
Dir::Right => {
|
Dir::Right => {
|
||||||
cursor.pos = (cursor.pos + 1).min(self.text.chars.len());
|
cursor.pos = (cursor.pos + dist[0]).min(self.text.chars.len());
|
||||||
cursor.reset_desired_col(&self.text);
|
cursor.reset_desired_col(&self.text);
|
||||||
}
|
}
|
||||||
Dir::Up => {
|
Dir::Up => {
|
||||||
|
|
@ -104,19 +145,30 @@ impl Buffer {
|
||||||
cursor.pos = 0;
|
cursor.pos = 0;
|
||||||
cursor.reset_desired_col(&self.text);
|
cursor.reset_desired_col(&self.text);
|
||||||
} else {
|
} else {
|
||||||
cursor.pos = self.text.to_pos([cursor.desired_col, coord[1] - 1]);
|
cursor.pos = self
|
||||||
|
.text
|
||||||
|
.to_pos([cursor.desired_col, coord[1] - dist[1] as isize]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Dir::Down => {
|
Dir::Down => {
|
||||||
let mut coord = self.text.to_coord(cursor.pos);
|
let mut coord = self.text.to_coord(cursor.pos);
|
||||||
cursor.pos = self.text.to_pos([cursor.desired_col, coord[1] + 1]);
|
cursor.pos = self
|
||||||
|
.text
|
||||||
|
.to_pos([cursor.desired_col, coord[1] + dist[1] as isize]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !retain_base {
|
||||||
|
cursor.base = cursor.pos;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, pos: usize, c: char) {
|
pub fn insert(&mut self, pos: usize, c: char) {
|
||||||
self.text.chars.insert(pos.min(self.text.chars.len()), c);
|
self.text.chars.insert(pos.min(self.text.chars.len()), c);
|
||||||
self.cursors.values_mut().for_each(|cursor| {
|
self.cursors.values_mut().for_each(|cursor| {
|
||||||
|
if cursor.base >= pos {
|
||||||
|
cursor.base += 1;
|
||||||
|
}
|
||||||
if cursor.pos >= pos {
|
if cursor.pos >= pos {
|
||||||
cursor.pos += 1;
|
cursor.pos += 1;
|
||||||
cursor.reset_desired_col(&self.text);
|
cursor.reset_desired_col(&self.text);
|
||||||
|
|
@ -124,35 +176,69 @@ impl Buffer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, pos: usize) {
|
pub fn enter(&mut self, cursor_id: CursorId, c: char) {
|
||||||
|
let Some(cursor) = self.cursors.get(cursor_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(selection) = cursor.selection() {
|
||||||
|
self.remove(selection);
|
||||||
|
self.enter(cursor_id, c);
|
||||||
|
} else {
|
||||||
|
self.insert(cursor.pos, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assumes range is well-formed
|
||||||
|
pub fn remove(&mut self, range: Range<usize>) {
|
||||||
// TODO: Bell if false?
|
// TODO: Bell if false?
|
||||||
if self.text.chars.len() > pos {
|
self.text.chars.drain(range.clone());
|
||||||
self.text.chars.remove(pos);
|
self.cursors.values_mut().for_each(|cursor| {
|
||||||
self.cursors.values_mut().for_each(|cursor| {
|
if cursor.base >= range.start {
|
||||||
if cursor.pos >= pos {
|
cursor.base = cursor
|
||||||
cursor.pos = cursor.pos.saturating_sub(1);
|
.base
|
||||||
cursor.reset_desired_col(&self.text);
|
.saturating_sub(range.end - range.start)
|
||||||
}
|
.max(range.start);
|
||||||
});
|
}
|
||||||
|
if cursor.pos >= range.start {
|
||||||
|
cursor.pos = cursor
|
||||||
|
.pos
|
||||||
|
.saturating_sub(range.end - range.start)
|
||||||
|
.max(range.start);
|
||||||
|
cursor.reset_desired_col(&self.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn backspace(&mut self, cursor_id: CursorId) {
|
||||||
|
let Some(cursor) = self.cursors.get(cursor_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(selection) = cursor.selection() {
|
||||||
|
self.remove(selection);
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = cursor.pos.checked_sub(1) {
|
||||||
|
self.remove(pos..pos + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn backspace(&mut self, pos: usize) {
|
pub fn delete(&mut self, cursor_id: CursorId) {
|
||||||
if let Some(pos) = pos.checked_sub(1) {
|
let Some(cursor) = self.cursors.get(cursor_id) else {
|
||||||
self.remove(pos);
|
return;
|
||||||
|
};
|
||||||
|
if let Some(selection) = cursor.selection() {
|
||||||
|
self.remove(selection);
|
||||||
|
} else {
|
||||||
|
self.remove(cursor.pos..cursor.pos + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete(&mut self, pos: usize) {
|
|
||||||
self.remove(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn start_session(&mut self) -> CursorId {
|
pub fn start_session(&mut self) -> CursorId {
|
||||||
self.cursors.insert(Cursor::default())
|
self.cursors.insert(Cursor::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end_session(&mut self, cursor: CursorId) {
|
pub fn end_session(&mut self, cursor_id: CursorId) {
|
||||||
self.cursors.remove(cursor);
|
self.cursors.remove(cursor_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,19 +187,20 @@ impl<'a> Rect<'a> {
|
||||||
|
|
||||||
pub fn text<C: Borrow<char>>(
|
pub fn text<C: Borrow<char>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
origin: [usize; 2],
|
origin: [isize; 2],
|
||||||
text: impl IntoIterator<Item = C>,
|
text: impl IntoIterator<Item = C>,
|
||||||
) -> Rect {
|
) -> Rect {
|
||||||
for (idx, c) in text.into_iter().enumerate() {
|
for (idx, c) in text.into_iter().enumerate() {
|
||||||
if origin[0] + idx >= self.size()[0] {
|
if (0..self.size()[0] as isize).contains(&(origin[0] + idx as isize)) && origin[1] >= 0
|
||||||
break;
|
{
|
||||||
} else {
|
|
||||||
let cell = Cell {
|
let cell = Cell {
|
||||||
c: *c.borrow(),
|
c: *c.borrow(),
|
||||||
fg: self.fg,
|
fg: self.fg,
|
||||||
bg: self.bg,
|
bg: self.bg,
|
||||||
};
|
};
|
||||||
if let Some(c) = self.get_mut([origin[0] + idx, origin[1]]) {
|
if let Some(c) =
|
||||||
|
self.get_mut([(origin[0] + idx as isize) as usize, origin[1] as usize])
|
||||||
|
{
|
||||||
*c = cell;
|
*c = cell;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +350,12 @@ impl<'a> Terminal<'a> {
|
||||||
stdout.queue(style::SetBackgroundColor(bg)).unwrap();
|
stdout.queue(style::SetBackgroundColor(bg)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout.queue(style::Print(self.fb[0].cells[pos].c)).unwrap();
|
// Convert non-printable chars
|
||||||
|
let c = match self.fb[0].cells[pos].c {
|
||||||
|
c if c.is_whitespace() => ' ',
|
||||||
|
c => c,
|
||||||
|
};
|
||||||
|
stdout.queue(style::Print(c)).unwrap();
|
||||||
|
|
||||||
// Move cursor
|
// Move cursor
|
||||||
cursor_pos[0] += 1;
|
cursor_pos[0] += 1;
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ pub struct Theme {
|
||||||
pub margin_line_num: Color,
|
pub margin_line_num: Color,
|
||||||
pub border: BorderTheme,
|
pub border: BorderTheme,
|
||||||
pub focus_border: BorderTheme,
|
pub focus_border: BorderTheme,
|
||||||
|
pub text: Color,
|
||||||
|
pub whitespace: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Theme {
|
impl Default for Theme {
|
||||||
|
|
@ -53,6 +55,8 @@ impl Default for Theme {
|
||||||
fg: Color::White,
|
fg: Color::White,
|
||||||
..BorderTheme::default()
|
..BorderTheme::default()
|
||||||
},
|
},
|
||||||
|
text: Color::Reset,
|
||||||
|
whitespace: Color::AnsiValue(245),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,7 @@ impl Input {
|
||||||
|
|
||||||
impl Element for Input {
|
impl Element for Input {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||||
match event.to_action(|e| {
|
match event.to_action(|e| e.to_char().map(Action::Char).or_else(|| e.to_move())) {
|
||||||
e.to_char()
|
|
||||||
.map(Action::Char)
|
|
||||||
.or_else(|| e.to_move().map(Action::Move))
|
|
||||||
}) {
|
|
||||||
Some(Action::Char('\x08')) => {
|
Some(Action::Char('\x08')) => {
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
if self.text.len() > self.cursor {
|
if self.text.len() > self.cursor {
|
||||||
|
|
@ -33,11 +29,11 @@ impl Element for Input {
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Move(Dir::Left)) => {
|
Some(Action::Move(Dir::Left, _, _)) => {
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Move(Dir::Right)) => {
|
Some(Action::Move(Dir::Right, _, _)) => {
|
||||||
self.cursor = (self.cursor + 1).min(self.text.len());
|
self.cursor = (self.cursor + 1).min(self.text.len());
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +43,7 @@ impl Element for Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Input {
|
impl Visual for Input {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.with(|frame| {
|
frame.with(|frame| {
|
||||||
frame.fill(' ');
|
frame.fill(' ');
|
||||||
frame.text([0, 0], self.preamble.chars());
|
frame.text([0, 0], self.preamble.chars());
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ pub trait Element<CanEnd = CannotEnd> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Visual {
|
pub trait Visual {
|
||||||
fn render(&self, state: &State, frame: &mut Rect);
|
fn render(&mut self, state: &State, frame: &mut Rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Label(String);
|
pub struct Label(String);
|
||||||
|
|
@ -76,10 +76,10 @@ impl std::ops::Deref for Label {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Label {
|
impl Visual for Label {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| {
|
frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| {
|
||||||
for (idx, line) in self.lines().enumerate() {
|
for (idx, line) in self.lines().enumerate() {
|
||||||
frame.text([0, idx], line.chars());
|
frame.text([0, idx as isize], line.chars());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ pub struct Doc {
|
||||||
cursors: HashMap<BufferId, CursorId>,
|
cursors: HashMap<BufferId, CursorId>,
|
||||||
// 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
|
||||||
focus: [isize; 2],
|
focus: [isize; 2],
|
||||||
|
// Remember the last known size for things like scrolling
|
||||||
|
last_size: [usize; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Doc {
|
impl Doc {
|
||||||
|
|
@ -23,6 +25,7 @@ impl Doc {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
focus: [0, 0],
|
focus: [0, 0],
|
||||||
|
last_size: [1, 1],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,13 +37,20 @@ impl Doc {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||||
self.focus[0] = self.focus[0].clamp(cursor_coord[0] - 20, cursor_coord[0] - 4);
|
for i in 0..2 {
|
||||||
self.focus[1] = self.focus[1].clamp(cursor_coord[1] - 20, cursor_coord[1] - 4);
|
self.focus[i] = self.focus[i].clamp(
|
||||||
|
cursor_coord[i] - self.last_size[i] as isize + 1,
|
||||||
|
cursor_coord[i],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(self, state: &mut State) {
|
pub fn close(self, state: &mut State) {
|
||||||
for (buffer, cursor) in self.cursors {
|
for (buffer, cursor) in self.cursors {
|
||||||
state.buffers[buffer].end_session(cursor);
|
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
buffer.end_session(cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +64,7 @@ impl Element for Doc {
|
||||||
match event.to_action(|e| {
|
match event.to_action(|e| {
|
||||||
e.to_char()
|
e.to_char()
|
||||||
.map(Action::Char)
|
.map(Action::Char)
|
||||||
.or_else(|| e.to_move().map(Action::Move))
|
.or_else(|| e.to_move())
|
||||||
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
||||||
.or_else(|| e.to_open_switcher())
|
.or_else(|| e.to_open_switcher())
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -72,21 +82,24 @@ impl Element for Doc {
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Char(c)) => {
|
Some(Action::Char(c)) => {
|
||||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
let cursor_id = self.cursors[&self.buffer];
|
||||||
return Err(event);
|
|
||||||
};
|
|
||||||
if c == '\x08' {
|
if c == '\x08' {
|
||||||
buffer.backspace(cursor.pos);
|
buffer.backspace(cursor_id);
|
||||||
} else if c == '\x7F' {
|
} else if c == '\x7F' {
|
||||||
buffer.delete(cursor.pos);
|
buffer.delete(cursor_id);
|
||||||
} else {
|
} else {
|
||||||
buffer.insert(cursor.pos, c);
|
buffer.enter(cursor_id, c);
|
||||||
}
|
}
|
||||||
self.refocus(state);
|
self.refocus(state);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Move(dir)) => {
|
Some(Action::Move(dir, page, retain_base)) => {
|
||||||
buffer.move_cursor(self.cursors[&self.buffer], dir);
|
let dist = if page {
|
||||||
|
self.last_size.map(|s| s.saturating_sub(3).max(1))
|
||||||
|
} else {
|
||||||
|
[1, 1]
|
||||||
|
};
|
||||||
|
buffer.move_cursor(self.cursors[&self.buffer], dir, dist, retain_base);
|
||||||
self.refocus(state);
|
self.refocus(state);
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +109,7 @@ impl Element for Doc {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Doc {
|
impl Visual for Doc {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -105,11 +118,20 @@ impl Visual for Doc {
|
||||||
};
|
};
|
||||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||||
|
|
||||||
let line_num_w = buffer.text.lines().count().ilog10() as usize + 1;
|
let line_num_w = buffer.text.lines().count().max(1).ilog10() as usize + 1;
|
||||||
|
let margin_w = line_num_w + 2;
|
||||||
|
|
||||||
for (i, (line_num, line)) in buffer
|
self.last_size = [frame.size()[0] - margin_w, frame.size()[1]];
|
||||||
|
|
||||||
|
let mut pos = 0;
|
||||||
|
for (i, (line_num, (line_pos, line))) in buffer
|
||||||
.text
|
.text
|
||||||
.lines()
|
.lines()
|
||||||
|
.map(move |line| {
|
||||||
|
let line_pos = pos;
|
||||||
|
pos += line.len();
|
||||||
|
(line_pos, line)
|
||||||
|
})
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.skip(self.focus[1].max(0) as usize)
|
.skip(self.focus[1].max(0) as usize)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
|
@ -117,7 +139,7 @@ impl Visual for Doc {
|
||||||
{
|
{
|
||||||
// Margin
|
// Margin
|
||||||
frame
|
frame
|
||||||
.rect([0, i], [line_num_w + 2, 1])
|
.rect([0, i], [margin_w, 1])
|
||||||
.with_bg(state.theme.margin_bg)
|
.with_bg(state.theme.margin_bg)
|
||||||
.with_fg(state.theme.margin_line_num)
|
.with_fg(state.theme.margin_line_num)
|
||||||
.fill(' ')
|
.fill(' ')
|
||||||
|
|
@ -125,14 +147,37 @@ impl Visual for Doc {
|
||||||
|
|
||||||
// Line
|
// Line
|
||||||
{
|
{
|
||||||
let mut frame = frame.rect([line_num_w + 2, i], [!0, 1]);
|
let mut frame = frame.rect([margin_w, i], [!0, 1]);
|
||||||
frame.text([0, 0], line);
|
for i in 0..frame.size()[0] {
|
||||||
|
let coord = self.focus[0] + i as isize;
|
||||||
|
if (0..line.len() as isize).contains(&coord) {
|
||||||
|
let pos = line_pos + coord as usize;
|
||||||
|
let selected = cursor.selection().map_or(false, |s| s.contains(&pos));
|
||||||
|
let (fg, c) = match line[coord as usize] {
|
||||||
|
'\n' if selected => (state.theme.whitespace, '⮠'),
|
||||||
|
c => (state.theme.text, c),
|
||||||
|
};
|
||||||
|
frame
|
||||||
|
.with_bg(if selected {
|
||||||
|
state.theme.select_bg
|
||||||
|
} else {
|
||||||
|
Color::Reset
|
||||||
|
})
|
||||||
|
.with_fg(fg)
|
||||||
|
.text([i as isize, 0], &[c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set cursor position
|
// Set cursor position
|
||||||
if cursor_coord[1] == line_num as isize {
|
if cursor_coord[1] == line_num as isize {
|
||||||
frame.set_cursor([cursor_coord[0], 0], CursorStyle::BlinkingBar);
|
frame.set_cursor(
|
||||||
|
[cursor_coord[0] - self.focus[0], 0],
|
||||||
|
CursorStyle::BlinkingBar,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pos += line.len();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,10 +245,12 @@ impl Element for Panes {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Panes {
|
impl Visual for Panes {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
for (i, pane) in self.panes.iter().enumerate() {
|
let n = self.panes.len();
|
||||||
let boundary = |i| frame.size()[0] * i / self.panes.len();
|
let frame_w = frame.size()[0];
|
||||||
|
let boundary = |i| frame_w * i / n;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
let is_selected = self.selected == i;
|
let is_selected = self.selected == i;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ impl Element<CanEnd> for Prompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Prompt {
|
impl Visual for Prompt {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.with(|f| self.input.render(state, f));
|
frame.with(|f| self.input.render(state, f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ impl Element<CanEnd> for Show {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Show {
|
impl Visual for Show {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
let lines = self.label.lines().count();
|
let lines = self.label.lines().count();
|
||||||
self.label.render(
|
self.label.render(
|
||||||
state,
|
state,
|
||||||
|
|
@ -99,7 +99,7 @@ impl Element<CanEnd> for Confirm {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Confirm {
|
impl Visual for Confirm {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
let lines = self.label.lines().count();
|
let lines = self.label.lines().count();
|
||||||
self.label.render(
|
self.label.render(
|
||||||
state,
|
state,
|
||||||
|
|
@ -118,16 +118,12 @@ pub struct Switcher {
|
||||||
|
|
||||||
impl Element<CanEnd> for Switcher {
|
impl Element<CanEnd> for Switcher {
|
||||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||||
match event.to_action(|e| {
|
match event.to_action(|e| e.to_cancel().or_else(|| e.to_go()).or_else(|| e.to_move())) {
|
||||||
e.to_cancel()
|
Some(Action::Move(Dir::Up, false, _)) => {
|
||||||
.or_else(|| e.to_go())
|
|
||||||
.or_else(|| e.to_move().map(Action::Move))
|
|
||||||
}) {
|
|
||||||
Some(Action::Move(Dir::Up)) => {
|
|
||||||
self.selected = (self.selected + self.options.len() - 1) % self.options.len();
|
self.selected = (self.selected + self.options.len() - 1) % self.options.len();
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
Some(Action::Move(Dir::Down)) => {
|
Some(Action::Move(Dir::Down, false, _)) => {
|
||||||
self.selected = (self.selected + 1) % self.options.len();
|
self.selected = (self.selected + 1) % self.options.len();
|
||||||
Ok(Resp::handled(None))
|
Ok(Resp::handled(None))
|
||||||
}
|
}
|
||||||
|
|
@ -146,7 +142,7 @@ impl Element<CanEnd> for Switcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Switcher {
|
impl Visual for Switcher {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
for (i, buffer) in self.options.iter().enumerate() {
|
for (i, buffer) in self.options.iter().enumerate() {
|
||||||
let Some(buffer) = state.buffers.get(*buffer) else {
|
let Some(buffer) = state.buffers.get(*buffer) else {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ impl Element<CanEnd> for Root {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Visual for Root {
|
impl Visual for Root {
|
||||||
fn render(&self, state: &State, frame: &mut Rect) {
|
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||||
frame.fill(' ');
|
frame.fill(' ');
|
||||||
|
|
||||||
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
||||||
|
|
@ -117,7 +117,7 @@ impl Visual for Root {
|
||||||
Some("Prompt (press alt + enter)"),
|
Some("Prompt (press alt + enter)"),
|
||||||
)
|
)
|
||||||
.with(|frame| {
|
.with(|frame| {
|
||||||
if let Some(Task::Prompt(p)) = self.tasks.last() {
|
if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
|
||||||
p.render(state, frame);
|
p.render(state, frame);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -129,7 +129,7 @@ impl Visual for Root {
|
||||||
self.panes.render(state, frame);
|
self.panes.render(state, frame);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(task) = self.tasks.last() {
|
if let Some(task) = self.tasks.last_mut() {
|
||||||
match task {
|
match task {
|
||||||
Task::Prompt(_) => {} // Prompt isn't rendered, it's always rendered above
|
Task::Prompt(_) => {} // Prompt isn't rendered, it's always rendered above
|
||||||
Task::Show(s) => s.render(state, frame),
|
Task::Show(s) => s.render(state, frame),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue