Unify prompt and doc input
This commit is contained in:
parent
84056b5a74
commit
67dac456af
9 changed files with 337 additions and 292 deletions
|
|
@ -17,7 +17,8 @@ pub enum Action {
|
|||
PaneMove(Dir), // Move panes
|
||||
PaneOpen(Dir), // Create a new pane
|
||||
PaneClose, // Close the current pane
|
||||
Cancel, // Cancels the current context
|
||||
Cancel, // Cancels the current action
|
||||
Continue, // Continue past an info-only element (like a help screen)
|
||||
Go, // Search, accept, or select the current option
|
||||
Yes, // A binary confirmation is answered 'yes'
|
||||
No, // A binary confirmation is answered 'no'
|
||||
|
|
@ -234,6 +235,22 @@ impl RawEvent {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_continue(&self) -> Option<Action> {
|
||||
if matches!(
|
||||
&self.0,
|
||||
TerminalEvent::Key(KeyEvent {
|
||||
code: KeyCode::Esc | KeyCode::Enter | KeyCode::Char(' '),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
})
|
||||
) {
|
||||
Some(Action::Continue)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_no(&self) -> Option<Action> {
|
||||
if matches!(self.to_char(), Some('n' | 'N')) {
|
||||
Some(Action::No)
|
||||
|
|
|
|||
28
src/state.rs
28
src/state.rs
|
|
@ -1,8 +1,4 @@
|
|||
use crate::{
|
||||
Action, Args, Color, Dir, Error, Event, theme,
|
||||
ui::{self, Element as _, Resp},
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use crate::{Args, Dir, Error, theme};
|
||||
use slotmap::{HopSlotMap, new_key_type};
|
||||
use std::{io, ops::Range, path::PathBuf};
|
||||
|
||||
|
|
@ -33,10 +29,17 @@ impl Cursor {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Text {
|
||||
chars: Vec<char>,
|
||||
}
|
||||
|
||||
impl ToString for Text {
|
||||
fn to_string(&self) -> String {
|
||||
self.chars.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn to_coord(&self, pos: usize) -> [isize; 2] {
|
||||
let mut n = 0;
|
||||
|
|
@ -53,7 +56,7 @@ impl Text {
|
|||
[(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, coord: [isize; 2]) -> usize {
|
||||
if coord[1] < 0 {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -98,14 +101,15 @@ impl Text {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Buffer {
|
||||
pub path: PathBuf,
|
||||
pub path: Option<PathBuf>,
|
||||
pub text: Text,
|
||||
pub cursors: HopSlotMap<CursorId, Cursor>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new(path: PathBuf) -> Result<Self, Error> {
|
||||
pub fn from_file(path: PathBuf) -> Result<Self, Error> {
|
||||
let chars = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => s.chars().collect(),
|
||||
// If the file doesn't exist, create a new file
|
||||
|
|
@ -113,7 +117,7 @@ impl Buffer {
|
|||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
Ok(Self {
|
||||
path,
|
||||
path: Some(path),
|
||||
text: Text { chars },
|
||||
cursors: HopSlotMap::default(),
|
||||
})
|
||||
|
|
@ -139,7 +143,7 @@ impl Buffer {
|
|||
cursor.reset_desired_col(&self.text);
|
||||
}
|
||||
Dir::Up => {
|
||||
let mut coord = self.text.to_coord(cursor.pos);
|
||||
let coord = self.text.to_coord(cursor.pos);
|
||||
// Special case: pressing 'up' at the top of the screen resets the cursor to the beginning
|
||||
if coord[1] <= 0 {
|
||||
cursor.pos = 0;
|
||||
|
|
@ -151,7 +155,7 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
Dir::Down => {
|
||||
let mut coord = self.text.to_coord(cursor.pos);
|
||||
let coord = self.text.to_coord(cursor.pos);
|
||||
cursor.pos = self
|
||||
.text
|
||||
.to_pos([cursor.desired_col, coord[1] + dist[1] as isize]);
|
||||
|
|
@ -258,7 +262,7 @@ impl TryFrom<Args> for State {
|
|||
};
|
||||
|
||||
for path in args.paths {
|
||||
this.buffers.insert(Buffer::new(path)?);
|
||||
this.buffers.insert(Buffer::from_file(path)?);
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
|
|
|
|||
85
src/ui/doc.rs
Normal file
85
src/ui/doc.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
state::{BufferId, CursorId},
|
||||
terminal::CursorStyle,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Doc {
|
||||
buffer: BufferId,
|
||||
// Remember the cursor we use for each buffer
|
||||
cursors: HashMap<BufferId, CursorId>,
|
||||
input: Input,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
pub fn new(state: &mut State, buffer: BufferId) -> Self {
|
||||
Self {
|
||||
buffer,
|
||||
// TODO: Don't index directly
|
||||
cursors: [(buffer, state.buffers[buffer].start_session())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
input: Input::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self, state: &State) -> Option<String> {
|
||||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||
return None;
|
||||
};
|
||||
Some(buffer.path.as_ref()?.display().to_string())
|
||||
}
|
||||
|
||||
pub fn close(self, state: &mut State) {
|
||||
for (buffer, cursor) in self.cursors {
|
||||
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
||||
continue;
|
||||
};
|
||||
buffer.end_session(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Doc {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
|
||||
match event.to_action(|e| e.to_open_switcher()) {
|
||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
self.buffer = new_buffer;
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
// Start a new cursor session for this buffer if one doesn't exist
|
||||
let cursor_id = *self
|
||||
.cursors
|
||||
.entry(self.buffer)
|
||||
.or_insert_with(|| buffer.start_session());
|
||||
self.input.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
_ => {
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
self.input.handle(buffer, cursor_id, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Doc {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||
return;
|
||||
};
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
self.input.render(state, buffer, cursor_id, frame);
|
||||
}
|
||||
}
|
||||
210
src/ui/input.rs
210
src/ui/input.rs
|
|
@ -1,59 +1,193 @@
|
|||
use super::*;
|
||||
use crate::terminal::CursorStyle;
|
||||
use crate::{
|
||||
state::{Buffer, CursorId},
|
||||
terminal::CursorStyle,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Copy, Clone, Default)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Doc,
|
||||
Prompt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Input {
|
||||
pub text: Vec<char>,
|
||||
pub cursor: usize,
|
||||
pub preamble: &'static str,
|
||||
pub mode: Mode,
|
||||
// x/y location in the buffer that the pane is trying to focus on
|
||||
pub focus: [isize; 2],
|
||||
// Remember the last known size for things like scrolling
|
||||
pub last_size: [usize; 2],
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn get_text(&self) -> String {
|
||||
self.text.iter().copied().collect()
|
||||
pub fn prompt() -> Self {
|
||||
Self {
|
||||
mode: Mode::Prompt,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Input {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||
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);
|
||||
for i in 0..2 {
|
||||
self.focus[i] = self.focus[i].clamp(
|
||||
cursor_coord[i] - self.last_size[i] as isize + 1,
|
||||
cursor_coord[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
cursor_id: CursorId,
|
||||
event: Event,
|
||||
) -> Result<Resp, Event> {
|
||||
match event.to_action(|e| e.to_char().map(Action::Char).or_else(|| e.to_move())) {
|
||||
Some(Action::Char('\x08')) => {
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
if self.text.len() > self.cursor {
|
||||
self.text.remove(self.cursor);
|
||||
}
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Char(c)) => {
|
||||
self.text.insert(self.cursor, c);
|
||||
self.cursor += 1;
|
||||
if c == '\x08' {
|
||||
buffer.backspace(cursor_id);
|
||||
} else if c == '\x7F' {
|
||||
buffer.delete(cursor_id);
|
||||
} else {
|
||||
buffer.enter(cursor_id, c);
|
||||
}
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(Dir::Left, _, _)) => {
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(Dir::Right, _, _)) => {
|
||||
self.cursor = (self.cursor + 1).min(self.text.len());
|
||||
Some(Action::Move(dir, page, retain_base)) => {
|
||||
let dist = if page {
|
||||
self.last_size.map(|s| s.saturating_sub(3).max(1))
|
||||
} else {
|
||||
[1, 1]
|
||||
};
|
||||
buffer.move_cursor(cursor_id, dir, dist, retain_base);
|
||||
self.refocus(buffer, cursor_id);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
_ => Err(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Input {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
frame.with(|frame| {
|
||||
frame.fill(' ');
|
||||
frame.text([0, 0], self.preamble.chars());
|
||||
pub fn render(
|
||||
&mut self,
|
||||
state: &State,
|
||||
buffer: &Buffer,
|
||||
cursor_id: CursorId,
|
||||
frame: &mut Rect,
|
||||
) {
|
||||
let title = if let Some(path) = &buffer.path {
|
||||
Some(path.display().to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
frame
|
||||
.rect([self.preamble.chars().count(), 0], frame.size())
|
||||
.with(|frame| {
|
||||
frame.text([0, 0], &self.text);
|
||||
frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
|
||||
});
|
||||
});
|
||||
// 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::Doc => line_num_w + 2,
|
||||
};
|
||||
|
||||
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]];
|
||||
|
||||
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::Prompt => frame
|
||||
.rect([0, i], [1, 1])
|
||||
.with_bg(state.theme.margin_bg)
|
||||
.with_fg(state.theme.margin_line_num)
|
||||
.fill(' ')
|
||||
.text([0, 0], ">".chars()),
|
||||
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).chars()),
|
||||
};
|
||||
|
||||
// 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;
|
||||
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
|
||||
if cursor_coord[1] == line_num as isize {
|
||||
frame.set_cursor(
|
||||
[cursor_coord[0] - self.focus[0], 0],
|
||||
CursorStyle::BlinkingBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pos += line.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl Visual for Input {
|
||||
// fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
// frame.with(|frame| {
|
||||
// frame.fill(' ');
|
||||
// frame.text([0, 0], self.preamble.chars());
|
||||
|
||||
// frame
|
||||
// .rect([self.preamble.chars().count(), 0], frame.size())
|
||||
// .with(|frame| {
|
||||
// frame.text([0, 0], &self.text);
|
||||
// frame.set_cursor([self.cursor as isize, 0], CursorStyle::BlinkingBar);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod doc;
|
||||
mod input;
|
||||
mod panes;
|
||||
mod prompt;
|
||||
|
|
@ -5,6 +6,7 @@ mod root;
|
|||
mod status;
|
||||
|
||||
pub use self::{
|
||||
doc::Doc,
|
||||
input::Input,
|
||||
panes::Panes,
|
||||
prompt::{Confirm, Prompt, Show, Switcher},
|
||||
|
|
|
|||
209
src/ui/panes.rs
209
src/ui/panes.rs
|
|
@ -1,186 +1,5 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
state::{Buffer, BufferId, Cursor, CursorId},
|
||||
terminal::CursorStyle,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Doc {
|
||||
buffer: BufferId,
|
||||
// Remember the cursor we use for each buffer
|
||||
cursors: HashMap<BufferId, CursorId>,
|
||||
// x/y location in the buffer that the pane is trying to focus on
|
||||
focus: [isize; 2],
|
||||
// Remember the last known size for things like scrolling
|
||||
last_size: [usize; 2],
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
pub fn new(state: &mut State, buffer: BufferId) -> Self {
|
||||
Self {
|
||||
buffer,
|
||||
// TODO: Don't index directly
|
||||
cursors: [(buffer, state.buffers[buffer].start_session())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
focus: [0, 0],
|
||||
last_size: [1, 1],
|
||||
}
|
||||
}
|
||||
|
||||
fn refocus(&mut self, state: &mut State) {
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return;
|
||||
};
|
||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) else {
|
||||
return;
|
||||
};
|
||||
let cursor_coord = buffer.text.to_coord(cursor.pos);
|
||||
for i in 0..2 {
|
||||
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) {
|
||||
for (buffer, cursor) in self.cursors {
|
||||
let Some(buffer) = state.buffers.get_mut(buffer) else {
|
||||
continue;
|
||||
};
|
||||
buffer.end_session(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for Doc {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp, Event> {
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
|
||||
match event.to_action(|e| {
|
||||
e.to_char()
|
||||
.map(Action::Char)
|
||||
.or_else(|| e.to_move())
|
||||
.or_else(|| e.to_pane_move().map(Action::PaneMove))
|
||||
.or_else(|| e.to_open_switcher())
|
||||
}) {
|
||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
self.buffer = new_buffer;
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
return Err(event);
|
||||
};
|
||||
// Start a new cursor session for this buffer if one doesn't exist
|
||||
self.cursors
|
||||
.entry(self.buffer)
|
||||
.or_insert_with(|| buffer.start_session());
|
||||
self.refocus(state);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Char(c)) => {
|
||||
let cursor_id = self.cursors[&self.buffer];
|
||||
if c == '\x08' {
|
||||
buffer.backspace(cursor_id);
|
||||
} else if c == '\x7F' {
|
||||
buffer.delete(cursor_id);
|
||||
} else {
|
||||
buffer.enter(cursor_id, c);
|
||||
}
|
||||
self.refocus(state);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(dir, page, retain_base)) => {
|
||||
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);
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
_ => Err(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Doc {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let Some(buffer) = state.buffers.get(self.buffer) else {
|
||||
return;
|
||||
};
|
||||
let Some(cursor) = buffer.cursors.get(self.cursors[&self.buffer]) 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 = line_num_w + 2;
|
||||
|
||||
self.last_size = [frame.size()[0].saturating_sub(margin_w), frame.size()[1]];
|
||||
|
||||
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
|
||||
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).chars());
|
||||
|
||||
// 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;
|
||||
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
|
||||
if cursor_coord[1] == line_num as isize {
|
||||
frame.set_cursor(
|
||||
[cursor_coord[0] - self.focus[0], 0],
|
||||
CursorStyle::BlinkingBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pos += line.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::state::BufferId;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Pane {
|
||||
|
|
@ -188,20 +7,6 @@ pub enum Pane {
|
|||
Doc(Doc),
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
fn title(&self, state: &State) -> Option<String> {
|
||||
match self {
|
||||
Self::Empty => None,
|
||||
Self::Doc(doc) => {
|
||||
let Some(buffer) = state.buffers.get(doc.buffer) else {
|
||||
return None;
|
||||
};
|
||||
Some(buffer.path.display().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Panes {
|
||||
selected: usize,
|
||||
panes: Vec<Pane>,
|
||||
|
|
@ -262,7 +67,7 @@ impl Element for Panes {
|
|||
Ok(Resp::handled(None))
|
||||
}
|
||||
// Pass anything else through to the active pane
|
||||
err => {
|
||||
_ => {
|
||||
if let Some(pane) = self.panes.get_mut(self.selected) {
|
||||
// Pass to pane
|
||||
match pane {
|
||||
|
|
@ -287,18 +92,10 @@ impl Visual for Panes {
|
|||
for (i, pane) in self.panes.iter_mut().enumerate() {
|
||||
let (x0, x1) = (boundary(i), boundary(i + 1));
|
||||
|
||||
let is_selected = self.selected == i;
|
||||
let border_theme = if frame.has_focus() && is_selected {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
};
|
||||
|
||||
// Draw pane contents
|
||||
frame
|
||||
.rect([x0, 0], [x1 - x0, frame.size()[1]])
|
||||
.with_border(border_theme, pane.title(state).as_deref())
|
||||
.with_focus(is_selected)
|
||||
.with_focus(self.selected == i)
|
||||
.with(|frame| match pane {
|
||||
Pane::Empty => {}
|
||||
Pane::Doc(doc) => doc.render(state, frame),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
use super::*;
|
||||
use crate::state::BufferId;
|
||||
use std::str::FromStr;
|
||||
use crate::state::{Buffer, BufferId, CursorId};
|
||||
|
||||
pub struct Prompt {
|
||||
pub input: Input,
|
||||
buffer: Buffer,
|
||||
cursor_id: CursorId,
|
||||
input: Input,
|
||||
}
|
||||
|
||||
impl Prompt {
|
||||
pub fn new() -> Self {
|
||||
let mut buffer = Buffer::default();
|
||||
Self {
|
||||
cursor_id: buffer.start_session(),
|
||||
buffer,
|
||||
input: Input::prompt(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_action(&self) -> Option<Action> {
|
||||
match self.input.get_text().as_str() {
|
||||
match self.buffer.text.to_string().as_str() {
|
||||
// The root sees 'cancel' as an initiator for quitting
|
||||
"q" | "quit" => Some(Action::Cancel),
|
||||
"version" => Some(Action::Show(format!(
|
||||
|
|
@ -41,18 +51,21 @@ impl Element<CanEnd> for Prompt {
|
|||
} else {
|
||||
Ok(Resp::end(Action::Show(format!(
|
||||
"unknown command `{}`",
|
||||
self.input.get_text()
|
||||
self.buffer.text.to_string()
|
||||
))))
|
||||
}
|
||||
}
|
||||
_ => self.input.handle(state, event).map(Resp::into_can_end),
|
||||
_ => self
|
||||
.input
|
||||
.handle(&mut self.buffer, self.cursor_id, event)
|
||||
.map(Resp::into_can_end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Prompt {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
frame.with(|f| self.input.render(state, f));
|
||||
frame.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,9 +75,9 @@ pub struct Show {
|
|||
|
||||
impl Element<CanEnd> for Show {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
match event.to_action(|e| e.to_cancel()) {
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
_ => Err(event),
|
||||
match event.to_action(|e| e.to_continue()) {
|
||||
Some(Action::Continue) => Ok(Resp::end(None)),
|
||||
_ => Ok(Resp::handled(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -147,6 +160,10 @@ impl Visual for Switcher {
|
|||
let Some(buffer) = state.buffers.get(*buffer) else {
|
||||
continue;
|
||||
};
|
||||
let buffer_name = match &buffer.path {
|
||||
Some(path) => path.display().to_string(),
|
||||
None => format!("<Untitled>"),
|
||||
};
|
||||
frame
|
||||
.rect(
|
||||
[
|
||||
|
|
@ -161,7 +178,7 @@ impl Visual for Switcher {
|
|||
state.theme.ui_bg
|
||||
})
|
||||
.fill(' ')
|
||||
.text([0, 0], buffer.path.display().to_string().chars());
|
||||
.text([0, 0], buffer_name.chars());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,12 +65,7 @@ impl Element<CanEnd> for Root {
|
|||
match action {
|
||||
Action::OpenPrompt => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks.push(Task::Prompt(Prompt {
|
||||
input: Input {
|
||||
preamble: "> ",
|
||||
..Input::default()
|
||||
},
|
||||
}));
|
||||
self.tasks.push(Task::Prompt(Prompt::new()));
|
||||
}
|
||||
Action::OpenSwitcher => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
|
|
@ -106,24 +101,20 @@ impl Visual for Root {
|
|||
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
||||
|
||||
// Display status bar
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with_border(
|
||||
if task_has_focus {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
},
|
||||
Some("Prompt (press alt + enter)"),
|
||||
)
|
||||
.with(|frame| {
|
||||
if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
|
||||
p.render(state, frame);
|
||||
}
|
||||
});
|
||||
let status_size = if let Some(Task::Prompt(p)) = self.tasks.last_mut() {
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with(|frame| p.render(state, frame));
|
||||
3
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
frame
|
||||
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
|
||||
.rect(
|
||||
[0, 0],
|
||||
[frame.size()[0], frame.size()[1].saturating_sub(status_size)],
|
||||
)
|
||||
.with_focus(!task_has_focus)
|
||||
.with(|frame| {
|
||||
self.panes.render(state, frame);
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
use super::*;
|
||||
|
||||
pub struct Status;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue