Better switcher behaviour
This commit is contained in:
parent
0bd78521e3
commit
7aafdaa90f
8 changed files with 337 additions and 155 deletions
|
|
@ -11,22 +11,22 @@ pub enum Dir {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Action {
|
||||
Char(char), // Insert a character
|
||||
Backspace, // Backspace a character
|
||||
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base)
|
||||
PaneMove(Dir), // Move panes
|
||||
PaneOpen(Dir), // Create a new pane
|
||||
PaneClose, // Close the current pane
|
||||
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'
|
||||
Quit, // Quit the application
|
||||
OpenPrompt, // Open the command prompt
|
||||
OpenSwitcher, // Open the buffer switcher
|
||||
Show(String), // Display some arbitrary text to the user
|
||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||
Char(char), // Insert a character
|
||||
Backspace, // Backspace a character
|
||||
Move(Dir, bool, bool), // Move the cursor (dir, page, retain_base)
|
||||
PaneMove(Dir), // Move panes
|
||||
PaneOpen(Dir), // Create a new pane
|
||||
PaneClose, // Close the current pane
|
||||
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'
|
||||
Quit, // Quit the application
|
||||
OpenPrompt, // Open the command prompt
|
||||
OpenSwitcher, // Open the buffer switcher
|
||||
Show(Option<String>, String), // Display an optionally titled informational text box to the user
|
||||
SwitchBuffer(BufferId), // Switch the current pane to the given buffer
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -37,6 +37,12 @@ pub enum Event {
|
|||
Raw(RawEvent),
|
||||
}
|
||||
|
||||
impl From<Action> for Event {
|
||||
fn from(action: Action) -> Self {
|
||||
Self::Action(action)
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn from_raw(e: TerminalEvent) -> Self {
|
||||
Self::Raw(RawEvent(e))
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fn main() -> Result<(), Error> {
|
|||
// Have the UI handle events
|
||||
if ui
|
||||
.handle(&mut state, Event::from_raw(ev))
|
||||
.map_or(false, |r| r.should_end())
|
||||
.map_or(false, |r| r.into_ended().is_some())
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ impl Default for BorderTheme {
|
|||
pub struct Theme {
|
||||
pub ui_bg: Color,
|
||||
pub select_bg: Color,
|
||||
pub unfocus_select_bg: Color,
|
||||
pub margin_bg: Color,
|
||||
pub margin_line_num: Color,
|
||||
pub border: BorderTheme,
|
||||
|
|
@ -48,6 +49,7 @@ impl Default for Theme {
|
|||
Self {
|
||||
ui_bg: Color::AnsiValue(235),
|
||||
select_bg: Color::AnsiValue(23),
|
||||
unfocus_select_bg: Color::AnsiValue(240),
|
||||
margin_bg: Color::Reset,
|
||||
margin_line_num: Color::AnsiValue(245),
|
||||
border: BorderTheme::default(),
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ impl Element for Doc {
|
|||
};
|
||||
|
||||
match event.to_action(|e| e.to_open_switcher()) {
|
||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action)),
|
||||
action @ Some(Action::OpenSwitcher) => Ok(Resp::handled(action.map(Into::into))),
|
||||
Some(Action::SwitchBuffer(new_buffer)) => {
|
||||
self.buffer = new_buffer;
|
||||
let Some(buffer) = state.buffers.get_mut(self.buffer) else {
|
||||
|
|
|
|||
|
|
@ -152,10 +152,12 @@ impl Input {
|
|||
c => (state.theme.text, c),
|
||||
};
|
||||
frame
|
||||
.with_bg(if selected {
|
||||
.with_bg(if !selected {
|
||||
Color::Reset
|
||||
} else if frame.has_focus() {
|
||||
state.theme.select_bg
|
||||
} else {
|
||||
Color::Reset
|
||||
state.theme.unfocus_select_bg
|
||||
})
|
||||
.with_fg(fg)
|
||||
.text([i as isize, 0], &[c]);
|
||||
|
|
|
|||
159
src/ui/mod.rs
159
src/ui/mod.rs
|
|
@ -20,42 +20,51 @@ use crate::{
|
|||
};
|
||||
|
||||
pub enum CannotEnd {}
|
||||
pub struct CanEnd;
|
||||
|
||||
pub struct Resp<CanEnd = CannotEnd> {
|
||||
should_end: Option<CanEnd>,
|
||||
pub action: Option<Action>,
|
||||
pub struct Resp<End = CannotEnd> {
|
||||
ended: Option<End>,
|
||||
pub event: Option<Event>,
|
||||
}
|
||||
|
||||
impl Resp<CanEnd> {
|
||||
pub fn end(action: impl Into<Option<Action>>) -> Self {
|
||||
Self {
|
||||
should_end: Some(CanEnd),
|
||||
action: action.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_end(&self) -> bool {
|
||||
self.should_end.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Resp<T> {
|
||||
pub fn handled(action: impl Into<Option<Action>>) -> Self {
|
||||
Self {
|
||||
should_end: None,
|
||||
action: action.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_can_end(self) -> Resp<CanEnd> {
|
||||
impl Resp<CannotEnd> {
|
||||
pub fn into_can_end<End>(self) -> Resp<End> {
|
||||
Resp {
|
||||
should_end: None,
|
||||
action: self.action,
|
||||
ended: None,
|
||||
event: self.event,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<End> Resp<End> {
|
||||
pub fn end(event: Option<Event>) -> Self
|
||||
where
|
||||
End: Default,
|
||||
{
|
||||
Self::end_with(Default::default(), event)
|
||||
}
|
||||
|
||||
pub fn end_with(end: End, event: Option<Event>) -> Self {
|
||||
Self {
|
||||
ended: Some(end),
|
||||
event: event.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handled(event: Option<Event>) -> Self {
|
||||
Self {
|
||||
ended: None,
|
||||
event: event.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_end(&self) -> bool {
|
||||
self.ended.is_some()
|
||||
}
|
||||
pub fn into_ended(mut self) -> Option<End> {
|
||||
self.ended
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Element<CanEnd = CannotEnd> {
|
||||
/// Attempt to handle an event.
|
||||
///
|
||||
|
|
@ -77,12 +86,104 @@ impl std::ops::Deref for Label {
|
|||
}
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn requested_height(&self) -> usize {
|
||||
self.0.lines().count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Label {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
frame.with_bg(state.theme.ui_bg).fill(' ').with(|frame| {
|
||||
frame.with(|frame| {
|
||||
for (idx, line) in self.lines().enumerate() {
|
||||
frame.text([0, idx as isize], line.chars());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// List selection
|
||||
pub struct Options<T> {
|
||||
pub selected: usize,
|
||||
// (score, option)
|
||||
pub options: Vec<T>,
|
||||
pub ranking: Vec<usize>,
|
||||
}
|
||||
|
||||
impl<T> Options<T> {
|
||||
pub fn new(options: impl IntoIterator<Item = T>) -> Self {
|
||||
let (ranking, options) = options.into_iter().enumerate().unzip();
|
||||
Self {
|
||||
selected: 0,
|
||||
options,
|
||||
ranking,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_scoring<F: FnMut(&T) -> Option<u32>>(&mut self, mut f: F) {
|
||||
let mut ranking = self
|
||||
.options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, o)| Some((i, f(o)?)))
|
||||
.collect::<Vec<_>>();
|
||||
ranking.sort_by_key(|(_, score)| *score);
|
||||
self.ranking = ranking.into_iter().map(|(i, _)| i).collect();
|
||||
}
|
||||
|
||||
pub fn requested_height(&self) -> usize {
|
||||
2 + self.ranking.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Element<T> for Options<T> {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<T>, Event> {
|
||||
match event.to_action(|e| e.to_go().or_else(|| e.to_move())) {
|
||||
Some(Action::Move(Dir::Up, false, _)) => {
|
||||
self.selected = (self.selected + self.ranking.len() - 1) % self.ranking.len();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(Dir::Down, false, _)) => {
|
||||
self.selected = (self.selected + 1) % self.ranking.len();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Go) => {
|
||||
if self.selected < self.ranking.len() {
|
||||
Ok(Resp::end_with(
|
||||
self.options.remove(self.ranking[self.selected]),
|
||||
None,
|
||||
))
|
||||
} else {
|
||||
Err(event)
|
||||
}
|
||||
}
|
||||
_ => Err(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Visual> Visual for Options<T> {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let mut frame = frame.with_border(
|
||||
if frame.has_focus() {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
for (i, idx) in self.ranking.iter().enumerate() {
|
||||
let option = &mut self.options[*idx];
|
||||
frame
|
||||
.rect([0, i], [frame.size()[0], 1])
|
||||
.with_bg(if self.selected == i {
|
||||
state.theme.select_bg
|
||||
} else {
|
||||
Color::Reset
|
||||
})
|
||||
.fill(' ')
|
||||
.with(|f| option.render(state, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
200
src/ui/prompt.rs
200
src/ui/prompt.rs
|
|
@ -21,39 +21,48 @@ impl Prompt {
|
|||
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!(
|
||||
"{} {}",
|
||||
env!("CARGO_PKG_NAME"),
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))),
|
||||
"?" | "help" => Some(Action::Show(format!(
|
||||
"Temporary help info:\n\
|
||||
"version" => Some(Action::Show(
|
||||
Some(format!("Version")),
|
||||
format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
|
||||
)),
|
||||
"?" | "help" => Some(Action::Show(
|
||||
Some(format!("Help")),
|
||||
format!(
|
||||
"Temporary help info:\n\
|
||||
- quit\n\
|
||||
- version\n\
|
||||
- pane_move_left\n\
|
||||
- pane_move_right\n\
|
||||
- help"
|
||||
))),
|
||||
),
|
||||
)),
|
||||
"pane_move_left" => Some(Action::PaneMove(Dir::Left)),
|
||||
"pane_move_right" => Some(Action::PaneMove(Dir::Right)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn requested_height(&self) -> usize {
|
||||
3
|
||||
}
|
||||
}
|
||||
|
||||
impl Element<CanEnd> for Prompt {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
impl Element<()> for Prompt {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||
match event.to_action(|e| e.to_go().or_else(|| e.to_cancel())) {
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
Some(Action::Go) => {
|
||||
if let Some(action) = self.get_action() {
|
||||
self.buffer.clear();
|
||||
Ok(Resp::handled(action))
|
||||
Ok(Resp::end(Some(action.into())))
|
||||
} else {
|
||||
Ok(Resp::handled(Action::Show(format!(
|
||||
"unknown command `{}`",
|
||||
self.buffer.text.to_string()
|
||||
))))
|
||||
Ok(Resp::handled(Some(
|
||||
Action::Show(
|
||||
Some(format!("Error")),
|
||||
format!("unknown command `{}`", self.buffer.text.to_string()),
|
||||
)
|
||||
.into(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
_ => self
|
||||
|
|
@ -71,30 +80,44 @@ impl Visual for Prompt {
|
|||
}
|
||||
|
||||
pub struct Show {
|
||||
pub title: Option<String>,
|
||||
pub label: Label,
|
||||
}
|
||||
|
||||
impl Element<CanEnd> for Show {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
impl Show {
|
||||
pub fn requested_height(&self) -> usize {
|
||||
self.label.requested_height() + 2
|
||||
}
|
||||
}
|
||||
|
||||
impl Element<()> for Show {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||
match event.to_action(|e| {
|
||||
e.to_cancel()
|
||||
.or_else(|| e.to_continue())
|
||||
.or_else(|| e.to_char().map(Action::Char))
|
||||
}) {
|
||||
// Shows cannot be cancelled, so pass the cancel along to the parent task
|
||||
Some(Action::Cancel) => Ok(Resp::end(Some(Action::Cancel))),
|
||||
Some(Action::Cancel) => Ok(Resp::end(Some(Action::Cancel.into()))),
|
||||
// A continue ends the show
|
||||
Some(Action::Continue) => Ok(Resp::end(None)),
|
||||
// Pass attempts to type to the parent prompt task
|
||||
// TODO: Don't assume that a `Show` is always the child of the prompt
|
||||
Some(Action::Char(c)) => Ok(Resp::end(Some(Action::Char(c)))),
|
||||
_ => Ok(Resp::handled(None)),
|
||||
// All other events end the show and get passed to the parent
|
||||
_ => Ok(Resp::end(Some(event))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Show {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let mut frame = frame.with_border(
|
||||
if frame.has_focus() {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
},
|
||||
self.title.as_deref(),
|
||||
);
|
||||
|
||||
let lines = self.label.lines().count();
|
||||
self.label.render(
|
||||
state,
|
||||
|
|
@ -111,10 +134,16 @@ pub struct Confirm {
|
|||
pub action: Action,
|
||||
}
|
||||
|
||||
impl Element<CanEnd> for Confirm {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
impl Confirm {
|
||||
pub fn requested_height(&self) -> usize {
|
||||
self.label.requested_height() + 2
|
||||
}
|
||||
}
|
||||
|
||||
impl Element<()> for Confirm {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||
match event.to_action(|e| e.to_yes().or_else(|| e.to_no()).or_else(|| e.to_cancel())) {
|
||||
Some(Action::Yes) => Ok(Resp::end(Some(self.action.clone()))),
|
||||
Some(Action::Yes) => Ok(Resp::end(Some(self.action.clone().into()))),
|
||||
Some(Action::No | Action::Cancel) => Ok(Resp::end(None)),
|
||||
// All other events get swallowed
|
||||
_ => Ok(Resp::handled(None)),
|
||||
|
|
@ -124,6 +153,15 @@ impl Element<CanEnd> for Confirm {
|
|||
|
||||
impl Visual for Confirm {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let mut frame = frame.with_border(
|
||||
if frame.has_focus() {
|
||||
&state.theme.focus_border
|
||||
} else {
|
||||
&state.theme.border
|
||||
},
|
||||
Some("Question"),
|
||||
);
|
||||
|
||||
let lines = self.label.lines().count();
|
||||
self.label.render(
|
||||
state,
|
||||
|
|
@ -136,60 +174,80 @@ impl Visual for Confirm {
|
|||
}
|
||||
|
||||
pub struct Switcher {
|
||||
pub selected: usize,
|
||||
pub options: Vec<BufferId>,
|
||||
pub options: Options<BufferId>,
|
||||
// Filter
|
||||
pub buffer: Buffer,
|
||||
pub cursor_id: CursorId,
|
||||
pub input: Input,
|
||||
}
|
||||
|
||||
impl Element<CanEnd> for Switcher {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
match event.to_action(|e| e.to_cancel().or_else(|| e.to_go()).or_else(|| e.to_move())) {
|
||||
Some(Action::Move(Dir::Up, false, _)) => {
|
||||
self.selected = (self.selected + self.options.len() - 1) % self.options.len();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Move(Dir::Down, false, _)) => {
|
||||
self.selected = (self.selected + 1) % self.options.len();
|
||||
Ok(Resp::handled(None))
|
||||
}
|
||||
Some(Action::Go) => Ok(Resp::end(
|
||||
if let Some(buffer) = self.options.get(self.selected) {
|
||||
Some(Action::SwitchBuffer(*buffer))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)),
|
||||
impl Switcher {
|
||||
pub fn new(buffers: impl IntoIterator<Item = BufferId>) -> Self {
|
||||
let mut buffer = Buffer::default();
|
||||
Self {
|
||||
options: Options::new(buffers),
|
||||
cursor_id: buffer.start_session(),
|
||||
buffer,
|
||||
input: Input::prompt(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn requested_height(&self) -> usize {
|
||||
self.options.requested_height() + 3
|
||||
}
|
||||
}
|
||||
|
||||
impl Element<()> for Switcher {
|
||||
fn handle(&mut self, state: &mut State, event: Event) -> Result<Resp<()>, Event> {
|
||||
match event.to_action(|e| e.to_cancel()) {
|
||||
Some(Action::Cancel) => Ok(Resp::end(None)),
|
||||
// All other events get swallowed
|
||||
_ => Ok(Resp::handled(None)),
|
||||
_ => match self.options.handle(state, event).map(Resp::into_ended) {
|
||||
Ok(Some(buffer_id)) => Ok(Resp::end(Some(Action::SwitchBuffer(buffer_id).into()))),
|
||||
Ok(None) => Ok(Resp::handled(None)),
|
||||
Err(event) => {
|
||||
let res = self
|
||||
.input
|
||||
.handle(&mut self.buffer, self.cursor_id, event)
|
||||
.map(Resp::into_can_end);
|
||||
// Score entries
|
||||
let filter = self.buffer.text.to_string();
|
||||
self.options.apply_scoring(|b| {
|
||||
let Some(buffer) = state.buffers.get(*b) else {
|
||||
return None;
|
||||
};
|
||||
match buffer.path.as_ref() {
|
||||
Some(path) if path.display().to_string().contains(&filter) => Some(1),
|
||||
Some(_) => None,
|
||||
None => Some(0),
|
||||
}
|
||||
});
|
||||
res
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for Switcher {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
for (i, buffer) in self.options.iter().enumerate() {
|
||||
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(
|
||||
[
|
||||
0,
|
||||
frame.size()[1].saturating_sub(3 + self.options.len()) + i,
|
||||
],
|
||||
[frame.size()[0], 1],
|
||||
)
|
||||
.with_bg(if self.selected == i {
|
||||
state.theme.select_bg
|
||||
} else {
|
||||
state.theme.ui_bg
|
||||
})
|
||||
.fill(' ')
|
||||
.text([0, 0], buffer_name.chars());
|
||||
}
|
||||
frame
|
||||
.rect([0, 0], [frame.size()[0], frame.size()[1].saturating_sub(3)])
|
||||
.with(|f| self.options.render(state, f));
|
||||
frame
|
||||
.rect([0, frame.size()[1].saturating_sub(3)], [frame.size()[0], 3])
|
||||
.with(|f| self.input.render(state, &self.buffer, self.cursor_id, f));
|
||||
}
|
||||
}
|
||||
|
||||
impl Visual for BufferId {
|
||||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
let Some(buffer) = state.buffers.get(*self) else {
|
||||
return;
|
||||
};
|
||||
let buffer_name = match &buffer.path {
|
||||
Some(path) => path.display().to_string(),
|
||||
None => format!("<Untitled>"),
|
||||
};
|
||||
frame.text([0, 0], buffer_name.chars());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@ pub enum Task {
|
|||
Switcher(Switcher),
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub fn requested_height(&self) -> usize {
|
||||
match self {
|
||||
Self::Prompt(p) => p.requested_height(),
|
||||
Self::Show(s) => s.requested_height(),
|
||||
Self::Confirm(c) => c.requested_height(),
|
||||
Self::Switcher(s) => s.requested_height(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Root {
|
||||
pub fn new(state: &mut State, buffers: &[BufferId]) -> Self {
|
||||
Self {
|
||||
|
|
@ -24,8 +35,8 @@ impl Root {
|
|||
}
|
||||
}
|
||||
|
||||
impl Element<CanEnd> for Root {
|
||||
fn handle(&mut self, state: &mut State, mut event: Event) -> Result<Resp<CanEnd>, Event> {
|
||||
impl Element<()> for Root {
|
||||
fn handle(&mut self, state: &mut State, mut event: Event) -> Result<Resp<()>, Event> {
|
||||
// Pass the event down through the list of tasks until we meet one that can handle it
|
||||
let mut task_idx = self.tasks.len();
|
||||
let action = loop {
|
||||
|
|
@ -33,10 +44,8 @@ impl Element<CanEnd> for Root {
|
|||
Some(task_idx) => task_idx,
|
||||
None => {
|
||||
break match self.panes.handle(state, event) {
|
||||
Ok(resp) => resp.action,
|
||||
Err(event) => {
|
||||
event.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel()))
|
||||
}
|
||||
Ok(resp) => resp.event,
|
||||
Err(event) => Some(event),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -51,21 +60,23 @@ impl Element<CanEnd> for Root {
|
|||
match res {
|
||||
Ok(resp) => {
|
||||
// If the task has requested that it should end, kill it and all of its children
|
||||
if resp.should_end() {
|
||||
if resp.is_end() {
|
||||
self.tasks.truncate(task_idx);
|
||||
}
|
||||
if let Some(action) = resp.action {
|
||||
event = Event::Action(action);
|
||||
event = if let Some(event) = resp.event {
|
||||
event
|
||||
} else {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
}
|
||||
Err(e) => event = e,
|
||||
}
|
||||
};
|
||||
|
||||
// Handle 'top-level' actions
|
||||
if let Some(action) = action {
|
||||
if let Some(action) =
|
||||
action.and_then(|e| e.to_action(|e| e.to_open_prompt().or_else(|| e.to_cancel())))
|
||||
{
|
||||
match action {
|
||||
Action::OpenPrompt => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
|
|
@ -73,16 +84,17 @@ impl Element<CanEnd> for Root {
|
|||
}
|
||||
Action::OpenSwitcher => {
|
||||
self.tasks.clear(); // Prompt overrides all
|
||||
self.tasks.push(Task::Switcher(Switcher {
|
||||
selected: 0,
|
||||
options: state.buffers.keys().collect(),
|
||||
}));
|
||||
self.tasks
|
||||
.push(Task::Switcher(Switcher::new(state.buffers.keys())));
|
||||
}
|
||||
Action::Cancel => self.tasks.push(Task::Confirm(Confirm {
|
||||
label: Label("Are you sure you wish to quit? (y/n)".to_string()),
|
||||
action: Action::Quit,
|
||||
})),
|
||||
Action::Show(text) => self.tasks.push(Task::Show(Show { label: Label(text) })),
|
||||
Action::Show(title, text) => self.tasks.push(Task::Show(Show {
|
||||
title,
|
||||
label: Label(text),
|
||||
})),
|
||||
Action::Quit => return Ok(Resp::end(None)),
|
||||
action => {
|
||||
return self
|
||||
|
|
@ -102,35 +114,36 @@ impl Visual for Root {
|
|||
fn render(&mut self, state: &State, frame: &mut Rect) {
|
||||
frame.fill(' ');
|
||||
|
||||
let task_has_focus = matches!(self.tasks.last(), Some(Task::Prompt(_)));
|
||||
let task_has_focus = !self.tasks.is_empty();
|
||||
|
||||
// Display status bar
|
||||
let status_size = if let Some(Task::Prompt(p)) = self.tasks.first_mut() {
|
||||
// Determine how much space the active task should use
|
||||
let task_h = self.tasks.last().map_or(0, |t| t.requested_height());
|
||||
|
||||
// Render active task
|
||||
if let Some(task) = 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
|
||||
};
|
||||
.rect(
|
||||
[0, frame.size()[1].saturating_sub(task_h)],
|
||||
[frame.size()[0], task_h],
|
||||
)
|
||||
.with_focus(task_has_focus)
|
||||
.with(|frame| match task {
|
||||
Task::Prompt(p) => p.render(state, frame),
|
||||
Task::Show(s) => s.render(state, frame),
|
||||
Task::Confirm(c) => c.render(state, frame),
|
||||
Task::Switcher(s) => s.render(state, frame),
|
||||
});
|
||||
}
|
||||
|
||||
// Render panes
|
||||
frame
|
||||
.rect(
|
||||
[0, 0],
|
||||
[frame.size()[0], frame.size()[1].saturating_sub(status_size)],
|
||||
[frame.size()[0], frame.size()[1].saturating_sub(task_h)],
|
||||
)
|
||||
.with_focus(!task_has_focus)
|
||||
.with(|frame| {
|
||||
self.panes.render(state, frame);
|
||||
});
|
||||
|
||||
if let Some(task) = self.tasks.last_mut() {
|
||||
match task {
|
||||
Task::Prompt(_) => {} // Prompt isn't rendered, it's always rendered above
|
||||
Task::Show(s) => s.render(state, frame),
|
||||
Task::Confirm(c) => c.render(state, frame),
|
||||
Task::Switcher(s) => s.render(state, frame),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue