Added page scrolling and cursor selection

This commit is contained in:
Joshua Barretto 2025-06-07 23:00:25 +01:00
parent d96fc47476
commit 81ab27cbbf
9 changed files with 251 additions and 104 deletions

View file

@ -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> {

View file

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

View file

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

View file

@ -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),
} }
} }
} }

View file

@ -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());

View file

@ -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());
} }
}); });
} }

View file

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

View file

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

View file

@ -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),