1
0
Fork 0

Compare commits

...

5 Commits

29 changed files with 435 additions and 280 deletions

View File

@ -5,6 +5,7 @@ use crossterm::event::{Event, KeyEventKind};
use crate::input::keymap::KeyBinding;
use crate::state::{Environment, State};
use crate::state::action::ActionResult;
use crate::state::event::EventResult;
use crate::state::view::View;
pub fn run(start_path: &Path) -> std::io::Result<()> {
@ -14,41 +15,55 @@ pub fn run(start_path: &Path) -> std::io::Result<()> {
let environment = Environment::try_from(&view)?;
let mut state = State::with_root_path(start_path, environment);
'render: loop {
loop {
match state.handle_events() {
EventResult::Nothing => {}
EventResult::Draw => {
view.set_dirty(false);
}
EventResult::Redraw => {
view.set_dirty(true);
}
}
view.render(|frame| state.render(frame))?;
'event: loop {
match handle_event(&mut state, crossterm::event::read()?) {
ActionResult::Nothing => {
continue 'event;
}
ActionResult::Draw => {
continue 'render;
}
ActionResult::Redraw => {
view.clear()?;
continue 'render;
}
ActionResult::PushLayer(layer) => {
state.push_layer(layer);
continue 'render;
}
ActionResult::ReplaceLayer(layer) => {
state.pop_layer();
state.push_layer(layer);
continue 'render;
}
ActionResult::PopLayer => {
if state.pop_layer() {
break 'render;
} else {
continue 'render;
}
match handle_terminal_event(&mut state, crossterm::event::read()?) {
ActionResult::Nothing => {
continue;
}
ActionResult::Draw => {
view.set_dirty(false);
continue;
}
ActionResult::Redraw => {
view.set_dirty(true);
continue;
}
ActionResult::PushLayer(layer) => {
state.push_layer(layer);
view.set_dirty(false);
continue;
}
ActionResult::ReplaceLayer(layer) => {
state.pop_layer();
state.push_layer(layer);
view.set_dirty(false);
continue;
}
ActionResult::PopLayer => {
if state.pop_layer() {
break;
} else {
view.set_dirty(false);
continue;
}
}
}
@ -58,7 +73,7 @@ pub fn run(start_path: &Path) -> std::io::Result<()> {
}
#[allow(clippy::needless_pass_by_value)]
fn handle_event(state: &mut State, event: Event) -> ActionResult {
fn handle_terminal_event(state: &mut State, event: Event) -> ActionResult {
if let Event::Key(key) = event {
if key.kind == KeyEventKind::Release {
ActionResult::Nothing

View File

@ -69,7 +69,7 @@ impl<'a> InputFieldDialogBuilder5<'a> {
self
}
pub fn build(self, confirm_action: Box<dyn Fn(String) -> ActionResult>) -> InputFieldDialogLayer<'a> {
pub fn on_confirm<F>(self, confirm_action: F) -> InputFieldDialogLayer<'a> where F: Fn(String) -> ActionResult + 'static {
let step4 = self.step4;
let step3 = step4.step3;
let step2 = step3.step2;

View File

@ -12,6 +12,7 @@ use crate::component::input::InputField;
use crate::input::keymap::KeyBinding;
use crate::state::action::ActionResult;
use crate::state::Environment;
use crate::state::event::EventResult;
use crate::state::layer::Layer;
use crate::state::view::Frame;
@ -29,8 +30,9 @@ pub struct InputFieldDialogLayer<'a> {
}
impl<'a> InputFieldDialogLayer<'a> {
fn new(y: u16, min_width: u16, default_color: Color, darker_color: Color, title: Line<'a>, message: Text<'a>, initial_value: Option<String>, confirm_action: Box<dyn Fn(String) -> ActionResult>) -> Self {
fn new<F>(y: u16, min_width: u16, default_color: Color, darker_color: Color, title: Line<'a>, message: Text<'a>, initial_value: Option<String>, confirm_action: F) -> Self where F: Fn(String) -> ActionResult + 'static {
let field = initial_value.map_or_else(InputField::new, InputField::with_text);
let confirm_action = Box::new(confirm_action);
Self { y, min_width, default_color, darker_color, title, message, field, confirm_action }
}
@ -58,6 +60,10 @@ impl<'a> Layer for InputFieldDialogLayer<'a> {
}
}
fn handle_events(&mut self, _environment: &Environment) -> EventResult {
EventResult::Nothing
}
fn render(&mut self, frame: &mut Frame) {
let message_width = u16::try_from(self.message.width()).unwrap_or(u16::MAX);
let message_height = u16::try_from(self.message.height()).unwrap_or(u16::MAX);

View File

@ -37,9 +37,9 @@ impl<'a> MessageDialogActionMap<'a> {
])
}
pub fn yes_no(yes_action: Box<dyn Fn() -> ActionResult>) -> Self {
pub fn yes_no<F>(yes_action: F) -> Self where F: Fn() -> ActionResult + 'static {
let mut map = ActionHashMap::new();
map.insert(KeyBinding::char('y'), yes_action);
map.insert(KeyBinding::char('y'), Box::new(yes_action));
map.insert(KeyBinding::char('n'), Box::new(|| ActionResult::PopLayer));
Self::new(map, vec![

View File

@ -63,7 +63,7 @@ impl<'a> MessageDialogBuilder4<'a> {
self.actions(MessageDialogActionMap::ok())
}
pub fn yes_no(self, yes_action: Box<dyn Fn() -> ActionResult>) -> MessageDialogLayer<'a> {
pub fn yes_no<F>(self, yes_action: F) -> MessageDialogLayer<'a> where F: Fn() -> ActionResult + 'static {
self.actions(MessageDialogActionMap::yes_no(yes_action))
}
}

View File

@ -10,6 +10,7 @@ use crate::component::dialog::render_dialog_border;
use crate::input::keymap::KeyBinding;
use crate::state::action::ActionResult;
use crate::state::Environment;
use crate::state::event::EventResult;
use crate::state::layer::Layer;
use crate::state::view::Frame;
@ -40,7 +41,7 @@ impl<'a> MessageDialogLayer<'a> {
MessageDialogBuilder
}
pub fn generic_error(y: u16, message: impl Into<Text<'a>>) -> MessageDialogLayer<'a> {
pub fn error(y: u16, message: impl Into<Text<'a>>) -> MessageDialogLayer<'a> {
Self::build()
.y(y)
.color(Color::LightRed)
@ -55,6 +56,10 @@ impl Layer for MessageDialogLayer<'_> {
self.actions.handle_input(key_binding)
}
fn handle_events(&mut self, _environment: &Environment) -> EventResult {
EventResult::Nothing
}
fn render(&mut self, frame: &mut Frame) {
let content_width = u16::try_from(self.message.width()).unwrap_or(u16::MAX);
let content_height = u16::try_from(self.message.height()).unwrap_or(u16::MAX);

View File

@ -17,7 +17,7 @@ impl Action<FsLayer> for PushCountDigit {
if new_count > MAX_COUNT {
layer.registers.count = None;
ActionResult::PushLayer(Box::new(MessageDialogLayer::generic_error(layer.dialog_y(), format!("Count is too large (> {MAX_COUNT}), it will be reset."))))
ActionResult::push_layer(MessageDialogLayer::error(layer.dialog_y(), format!("Count is too large (> {MAX_COUNT}), it will be reset.")))
} else {
layer.registers.count = Some(new_count);
ActionResult::Nothing

View File

@ -1,14 +1,12 @@
use std::{fs, io};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use ratatui::style::Color;
use slab_tree::NodeId;
use crate::component::dialog::input::InputFieldDialogLayer;
use crate::component::dialog::message::MessageDialogLayer;
use crate::component::filesystem::action::file::{FileNode, get_selected_file};
use crate::component::filesystem::event::FsLayerEvent;
use crate::component::filesystem::action::file::{FileNode, get_selected_file, RefreshParentDirectoryAndSelectFile};
use crate::component::filesystem::FsLayer;
use crate::file::FileKind;
use crate::state::action::{Action, ActionResult};
@ -71,7 +69,7 @@ impl Action<FsLayer> for CreateDirectoryInSelectedDirectory {
fn create_in_selected_directory<T: CreateEntry>(layer: &mut FsLayer) -> ActionResult {
if let Some(FileNode { node, path, .. }) = get_selected_file(layer).filter(|n| matches!(n.entry.kind(), FileKind::Directory)) {
ActionResult::PushLayer(Box::new(create_new_name_prompt::<T>(layer, path.to_owned(), node.node_id())))
ActionResult::push_layer(create_new_name_prompt::<T>(layer, path.to_owned(), node.node_id()))
} else {
ActionResult::Nothing
}
@ -95,7 +93,7 @@ impl Action<FsLayer> for CreateDirectoryInParentOfSelectedEntry {
fn create_in_parent_of_selected_file<T: CreateEntry>(layer: &mut FsLayer) -> ActionResult {
if let Some((parent_node_id, parent_path)) = get_parent_of_selected_file(layer) {
ActionResult::PushLayer(Box::new(create_new_name_prompt::<T>(layer, parent_path.to_owned(), parent_node_id)))
ActionResult::push_layer(create_new_name_prompt::<T>(layer, parent_path.to_owned(), parent_node_id))
} else {
ActionResult::Nothing
}
@ -105,9 +103,9 @@ fn get_parent_of_selected_file(layer: &FsLayer) -> Option<(NodeId, &Path)> {
get_selected_file(layer).and_then(|n| { Some((n.node.parent_id()?, n.path.parent()?)) })
}
fn create_new_name_prompt<'b, T: CreateEntry>(layer: &FsLayer, parent_folder: PathBuf, view_node_id_to_refresh: NodeId) -> InputFieldDialogLayer<'b> {
fn create_new_name_prompt<'b, T: CreateEntry>(layer: &FsLayer, parent_folder: PathBuf, parent_view_node_id: NodeId) -> InputFieldDialogLayer<'b> {
let y = layer.dialog_y();
let pending_events = Rc::clone(&layer.pending_events);
let events = layer.events();
InputFieldDialogLayer::build()
.y(y)
@ -115,25 +113,24 @@ fn create_new_name_prompt<'b, T: CreateEntry>(layer: &FsLayer, parent_folder: Pa
.color(Color::LightCyan, Color::Cyan)
.title(T::title())
.message(format!("Creating {} in {}", T::kind(), parent_folder.to_string_lossy()))
.build(Box::new(move |new_name| {
.on_confirm(move |new_name| {
if new_name.is_empty() {
return ActionResult::Nothing;
}
let new_path = parent_folder.join(&new_name);
if new_path.exists() {
return ActionResult::PushLayer(Box::new(MessageDialogLayer::generic_error(y, "Something with this name already exists.")));
return ActionResult::push_layer(MessageDialogLayer::error(y, "Something with this name already exists."));
}
match T::create(new_path) {
Ok(_) => {
FsLayerEvent::RefreshViewNodeChildren(view_node_id_to_refresh).enqueue(&pending_events);
FsLayerEvent::SelectViewNodeChildByFileName(view_node_id_to_refresh, new_name).enqueue(&pending_events);
events.enqueue(RefreshParentDirectoryAndSelectFile { parent_view_node_id, child_file_name: new_name });
ActionResult::PopLayer
}
Err(e) => {
ActionResult::PushLayer(Box::new(MessageDialogLayer::generic_error(y, format!("Could not create {}: {e}", T::kind()))))
ActionResult::push_layer(MessageDialogLayer::error(y, format!("Could not create {}: {e}", T::kind())))
}
}
}))
})
}

View File

@ -1,6 +1,5 @@
use std::io;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::{Duration, Instant};
use ratatui::style::Color;
@ -9,18 +8,18 @@ use slab_tree::NodeId;
use crate::component::dialog::message::MessageDialogLayer;
use crate::component::filesystem::action::file::{FileNode, get_entry_kind_name, get_selected_file};
use crate::component::filesystem::event::FsLayerEvent;
use crate::component::filesystem::FsLayer;
use crate::file::{FileEntry, FileKind};
use crate::state::action::{Action, ActionResult};
use crate::state::Environment;
use crate::state::event::EventResult;
pub struct DeleteSelectedEntry;
impl Action<FsLayer> for DeleteSelectedEntry {
fn perform(&self, layer: &mut FsLayer, _environment: &Environment) -> ActionResult {
if let Some(FileNode { node, entry, path }) = get_selected_file(layer) {
ActionResult::PushLayer(Box::new(create_delete_confirmation_dialog(layer, node.node_id(), entry, path.to_owned())))
ActionResult::push_layer(create_delete_confirmation_dialog(layer, node.node_id(), entry, path.to_owned()))
} else {
ActionResult::Nothing
}
@ -29,7 +28,7 @@ impl Action<FsLayer> for DeleteSelectedEntry {
fn create_delete_confirmation_dialog<'a>(layer: &FsLayer, view_node_id: NodeId, entry: &FileEntry, path: PathBuf) -> MessageDialogLayer<'a> {
let y = layer.dialog_y();
let pending_events = Rc::clone(&layer.pending_events);
let events = layer.events();
let total_files = if matches!(entry.kind(), FileKind::Directory) {
count_files(path.clone())
@ -45,17 +44,17 @@ fn create_delete_confirmation_dialog<'a>(layer: &FsLayer, view_node_id: NodeId,
Line::from(format!("Permanently delete {}?", path.to_string_lossy())),
Line::from(format!("This will affect {}.", total_files.describe())),
])
.yes_no(Box::new(move || {
.yes_no(move || {
match delete_path_recursively(&path) {
Ok(_) => {
FsLayerEvent::DeleteViewNode(view_node_id).enqueue(&pending_events);
events.enqueue_fn(move |layer, _| EventResult::draw_if(layer.tree.delete_node(view_node_id)));
ActionResult::PopLayer
}
Err(e) => {
ActionResult::ReplaceLayer(Box::new(MessageDialogLayer::generic_error(y.saturating_add(1), e.to_string())))
ActionResult::replace_layer(MessageDialogLayer::error(y.saturating_add(1), e.to_string()))
}
}
}))
})
}
const MAX_COUNT_TIME: Duration = Duration::from_secs(5);

View File

@ -8,11 +8,11 @@ use slab_tree::NodeRef;
use crate::component::dialog::message::MessageDialogLayer;
use crate::component::filesystem::action::file::{FileNode, get_selected_file};
use crate::component::filesystem::event::FsLayerEvent;
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::FsTreeViewNode;
use crate::state::action::{Action, ActionResult};
use crate::state::Environment;
use crate::state::event::EventResult;
use crate::util::slab_tree::NodeRefExtensions;
pub struct EditSelectedEntry;
@ -34,12 +34,12 @@ fn open_default_editor(layer: &FsLayer, node: &NodeRef<FsTreeViewNode>, path: &P
.status();
if status.is_err_and(|e| e.kind() == ErrorKind::NotFound) {
return ActionResult::PushLayer(Box::new(MessageDialogLayer::generic_error(layer.dialog_y(), format!("Default editor '{}' not found.", editor.to_string_lossy()))));
return ActionResult::push_layer(MessageDialogLayer::error(layer.dialog_y(), format!("Default editor '{}' not found.", editor.to_string_lossy())));
}
// Refresh the parent directory, or the root node if this is the view root.
let node_id_to_refresh = node.parent_id().unwrap_or_else(|| node.node_id());
FsLayerEvent::RefreshViewNodeChildren(node_id_to_refresh).enqueue(&layer.pending_events);
layer.events().enqueue_fn(move |layer, _| EventResult::draw_if(layer.tree.refresh_children(node_id_to_refresh)));
ActionResult::Redraw
}

View File

@ -1,11 +1,13 @@
use std::io;
use std::path::Path;
use slab_tree::NodeRef;
use slab_tree::{NodeId, NodeRef};
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::FsTreeViewNode;
use crate::file::{FileEntry, FileKind};
use crate::state::Environment;
use crate::state::event::{Event, EventResult};
pub use self::create::*;
pub use self::delete::*;
@ -18,7 +20,7 @@ mod edit;
mod rename;
fn get_selected_file(layer: &FsLayer) -> Option<FileNode> {
if let Some(node) = layer.selected_node() {
if let Some(node) = layer.tree.selected_node() {
if let Some(entry) = layer.tree.get_entry(&node) {
if let Some(path) = entry.path() {
return Some(FileNode { node, entry, path });
@ -54,3 +56,19 @@ fn format_io_error(err: &io::Error) -> String {
str.push('.');
str
}
struct RefreshParentDirectoryAndSelectFile {
parent_view_node_id: NodeId,
child_file_name: String,
}
impl Event<FsLayer> for RefreshParentDirectoryAndSelectFile {
fn dispatch(&self, layer: &mut FsLayer, _environment: &Environment) -> EventResult {
if layer.tree.refresh_children(self.parent_view_node_id) {
layer.tree.select_child_node_by_name(self.parent_view_node_id, &self.child_file_name);
EventResult::Draw
} else {
EventResult::Nothing
}
}
}

View File

@ -1,15 +1,13 @@
use io::ErrorKind;
use std::{fs, io};
use std::path::PathBuf;
use std::rc::Rc;
use ratatui::style::Color;
use slab_tree::NodeRef;
use crate::component::dialog::input::InputFieldDialogLayer;
use crate::component::dialog::message::MessageDialogLayer;
use crate::component::filesystem::action::file::{FileNode, format_io_error, get_entry_kind_name, get_selected_file};
use crate::component::filesystem::event::FsLayerEvent;
use crate::component::filesystem::action::file::{FileNode, format_io_error, get_entry_kind_name, get_selected_file, RefreshParentDirectoryAndSelectFile};
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::FsTreeViewNode;
use crate::file::FileEntry;
@ -24,7 +22,7 @@ pub struct RenameSelectedEntry {
impl Action<FsLayer> for RenameSelectedEntry {
fn perform(&self, layer: &mut FsLayer, _environment: &Environment) -> ActionResult {
if let Some(FileNode { node, entry, path }) = get_selected_file(layer) {
ActionResult::PushLayer(Box::new(self.create_rename_dialog(layer, &node, entry, path.to_owned())))
ActionResult::push_layer(self.create_rename_dialog(layer, &node, entry, path.to_owned()))
} else {
ActionResult::Nothing
}
@ -35,8 +33,8 @@ impl RenameSelectedEntry {
#[allow(clippy::wildcard_enum_match_arm)]
fn create_rename_dialog<'a, 'b>(&'a self, layer: &'a FsLayer, node: &'a NodeRef<FsTreeViewNode>, entry: &'a FileEntry, path: PathBuf) -> InputFieldDialogLayer<'b> {
let y = layer.dialog_y();
let parent_node_id = node.parent_id();
let pending_events = Rc::clone(&layer.pending_events);
let events = layer.events();
let parent_view_node_id = node.parent_id();
InputFieldDialogLayer::build()
.y(y)
@ -45,20 +43,19 @@ impl RenameSelectedEntry {
.title(format!("Rename {}", get_entry_kind_name(entry)))
.message(format!("Renaming {}", path.to_string_lossy()))
.initial_value(self.prefill.then(|| entry.name().str().to_owned()))
.build(Box::new(move |new_name| {
.on_confirm(move |new_name| {
match rename_file(&path, &new_name) {
Ok(_) => {
if let Some(parent_node_id) = parent_node_id {
FsLayerEvent::RefreshViewNodeChildren(parent_node_id).enqueue(&pending_events);
FsLayerEvent::SelectViewNodeChildByFileName(parent_node_id, new_name).enqueue(&pending_events);
if let Some(parent_view_node_id) = parent_view_node_id {
events.enqueue(RefreshParentDirectoryAndSelectFile { parent_view_node_id, child_file_name: new_name });
}
ActionResult::PopLayer
}
Err(e) => {
ActionResult::PushLayer(Box::new(MessageDialogLayer::generic_error(y.saturating_add(1), format_io_error(&e))))
ActionResult::push_layer(MessageDialogLayer::error(y.saturating_add(1), format_io_error(&e)))
}
}
}))
})
}
}

View File

@ -1,14 +1,15 @@
use slab_tree::NodeId;
use crate::component::filesystem::action::movement::{get_simple_movement_target, MovementAction, perform_movement_with_count, SimpleMovementAction};
use crate::component::filesystem::action::movement::{get_simple_movement_target, MovementAction, perform_movement_with_count_from_register, SimpleMovementAction};
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::FsTree;
use crate::state::Environment;
pub struct ExpandSelectedOr<M: SimpleMovementAction>(pub M);
impl<M: SimpleMovementAction> MovementAction for ExpandSelectedOr<M> {
fn get_target(&self, layer: &mut FsLayer, _environment: &Environment) -> Option<NodeId> where Self: Sized {
Some(perform_action_or_movement::<M, _>(layer, FsLayer::expand))
Some(perform_action_or_movement::<M, _>(layer, FsTree::expand))
}
}
@ -16,16 +17,16 @@ pub struct CollapseSelectedOr<M: SimpleMovementAction>(pub M);
impl<M: SimpleMovementAction> MovementAction for CollapseSelectedOr<M> {
fn get_target(&self, layer: &mut FsLayer, _environment: &Environment) -> Option<NodeId> where Self: Sized {
Some(perform_action_or_movement::<M, _>(layer, FsLayer::collapse))
Some(perform_action_or_movement::<M, _>(layer, FsTree::collapse))
}
}
fn perform_action_or_movement<M: SimpleMovementAction, F>(layer: &mut FsLayer, action: F) -> NodeId where F: Fn(&mut FsLayer, NodeId) -> bool {
perform_movement_with_count(layer, layer.registers.count, |layer, node_id| {
if action(layer, node_id) {
fn perform_action_or_movement<M: SimpleMovementAction, F>(layer: &mut FsLayer, action: F) -> NodeId where F: Fn(&mut FsTree, NodeId) -> bool {
perform_movement_with_count_from_register(layer, |tree, node_id| {
if action(tree, node_id) {
Some(node_id)
} else {
get_simple_movement_target::<M>(layer, node_id)
get_simple_movement_target::<M>(tree, node_id)
}
})
}

View File

@ -1,8 +1,8 @@
use slab_tree::{NodeId, NodeRef};
use crate::component::filesystem::action::movement::{MovementAction, perform_movement_with_count, SimpleMovementAction};
use crate::component::filesystem::action::movement::{MovementAction, perform_movement_with_count_from_register, SimpleMovementAction};
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::{FsTreeView, FsTreeViewNode};
use crate::component::filesystem::tree::{FsTree, FsTreeView, FsTreeViewNode};
use crate::state::Environment;
use crate::util::slab_tree::NodeRefExtensions;
@ -46,19 +46,17 @@ pub struct MoveOrTraverseUpParent;
impl MovementAction for MoveOrTraverseUpParent {
fn get_target(&self, layer: &mut FsLayer, _environment: &Environment) -> Option<NodeId> where Self: Sized {
Some(perform_movement_with_count(layer, layer.registers.count, Self::get_target))
Some(perform_movement_with_count_from_register(layer, Self::get_target))
}
}
impl MoveOrTraverseUpParent {
fn get_target(layer: &mut FsLayer, node_id: NodeId) -> Option<NodeId> {
let view = &layer.tree.view;
if let Some(node) = view.get(node_id) {
let target_node_id = <MoveToParent as SimpleMovementAction>::get_target(view, &node);
fn get_target(tree: &mut FsTree, node_id: NodeId) -> Option<NodeId> {
if let Some(node) = tree.view.get(node_id) {
let target_node_id = <MoveToParent as SimpleMovementAction>::get_target(&tree.view, &node);
if target_node_id.is_some() {
return target_node_id;
} else if let Some(target_node_id) = layer.traverse_up_root() {
} else if let Some(target_node_id) = tree.traverse_up_root() {
return Some(target_node_id)
}
}

View File

@ -2,7 +2,7 @@ use slab_tree::{NodeId, NodeRef};
use crate::component::filesystem::action::movement::{get_simple_movement_target, MovementAction, MoveToParent, perform_movement_with_count_from, SimpleMovementAction};
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::{FsTreeView, FsTreeViewNode};
use crate::component::filesystem::tree::{FsTree, FsTreeView, FsTreeViewNode};
use crate::state::Environment;
/// Moves up `count` lines (1 line by default).
@ -28,16 +28,15 @@ pub struct MoveToFirst;
impl MovementAction for MoveToFirst {
fn get_target(&self, layer: &mut FsLayer, _environment: &Environment) -> Option<NodeId> where Self: Sized {
Some(Self::get_target(layer))
Some(Self::get_target(&mut layer.tree))
}
}
impl MoveToFirst {
fn get_target(layer: &mut FsLayer) -> NodeId where Self: Sized {
let view = &layer.tree.view;
let mut target_node_id = layer.selected_view_node_id;
fn get_target(tree: &mut FsTree) -> NodeId where Self: Sized {
let mut target_node_id = tree.selected_view_node_id;
while let Some(node_id) = view.get(target_node_id).and_then(|node| <MoveToParent as SimpleMovementAction>::get_target(view, &node)) {
while let Some(node_id) = tree.view.get(target_node_id).and_then(|node| <MoveToParent as SimpleMovementAction>::get_target(&tree.view, &node)) {
target_node_id = node_id;
}
@ -50,7 +49,7 @@ pub struct MoveToLast;
impl MovementAction for MoveToLast {
fn get_target(&self, layer: &mut FsLayer, _environment: &Environment) -> Option<NodeId> where Self: Sized {
let first_node_id = MoveToFirst::get_target(layer);
let first_node_id = MoveToFirst::get_target(&mut layer.tree);
let last_node_id = layer.tree.view.get_last_descendant_or_self(first_node_id);
Some(last_node_id)
}
@ -63,9 +62,10 @@ pub struct MoveToLineOr<A: MovementAction>(pub A);
impl<A: MovementAction> MovementAction for MoveToLineOr<A> {
fn get_target(&self, layer: &mut FsLayer, environment: &Environment) -> Option<NodeId> where Self: Sized {
if let Some(line_number) = layer.registers.count {
let tree = &mut layer.tree;
let line_index = Some(line_number.saturating_sub(1));
let first_node_id = MoveToFirst::get_target(layer);
Some(perform_movement_with_count_from(layer, line_index, first_node_id, get_simple_movement_target::<MoveDown>))
let first_node_id = MoveToFirst::get_target(tree);
Some(perform_movement_with_count_from(tree, line_index, first_node_id, get_simple_movement_target::<MoveDown>))
} else {
self.0.get_target(layer, environment)
}

View File

@ -1,7 +1,7 @@
use slab_tree::{NodeId, NodeRef};
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::{FsTreeView, FsTreeViewNode};
use crate::component::filesystem::tree::{FsTree, FsTreeView, FsTreeViewNode};
use crate::state::action::{Action, ActionResult};
use crate::state::Environment;
@ -25,7 +25,7 @@ pub trait MovementAction {
impl<T: MovementAction> Action<FsLayer> for T {
fn perform(&self, layer: &mut FsLayer, environment: &Environment) -> ActionResult {
if let Some(target_node_id) = self.get_target(layer, environment) {
layer.selected_view_node_id = target_node_id;
layer.tree.selected_view_node_id = target_node_id;
ActionResult::Draw
} else {
ActionResult::Nothing
@ -33,15 +33,19 @@ impl<T: MovementAction> Action<FsLayer> for T {
}
}
fn perform_movement_with_count<F>(layer: &mut FsLayer, count: Option<usize>, get_target: F) -> NodeId where F: Fn(&mut FsLayer, NodeId) -> Option<NodeId> {
perform_movement_with_count_from(layer, count, layer.selected_view_node_id, get_target)
fn perform_movement_with_count_from_register<F>(layer: &mut FsLayer, get_target: F) -> NodeId where F: Fn(&mut FsTree, NodeId) -> Option<NodeId> {
perform_movement_with_count(&mut layer.tree, layer.registers.count, get_target)
}
fn perform_movement_with_count_from<F>(layer: &mut FsLayer, count: Option<usize>, start_node_id: NodeId, get_target: F) -> NodeId where F: Fn(&mut FsLayer, NodeId) -> Option<NodeId> {
fn perform_movement_with_count<F>(tree: &mut FsTree, count: Option<usize>, get_target: F) -> NodeId where F: Fn(&mut FsTree, NodeId) -> Option<NodeId> {
perform_movement_with_count_from(tree, count, tree.selected_view_node_id, get_target)
}
fn perform_movement_with_count_from<F>(tree: &mut FsTree, count: Option<usize>, start_node_id: NodeId, get_target: F) -> NodeId where F: Fn(&mut FsTree, NodeId) -> Option<NodeId> {
let mut target_node_id = start_node_id;
for _ in 0..count.unwrap_or(1) {
if let Some(node_id) = get_target(layer, target_node_id) {
if let Some(node_id) = get_target(tree, target_node_id) {
target_node_id = node_id;
} else {
break;
@ -57,11 +61,10 @@ pub trait SimpleMovementAction {
impl<T: SimpleMovementAction> MovementAction for T {
fn get_target(&self, layer: &mut FsLayer, _environment: &Environment) -> Option<NodeId> where Self: Sized {
Some(perform_movement_with_count(layer, layer.registers.count, get_simple_movement_target::<T>))
Some(perform_movement_with_count_from_register(layer, get_simple_movement_target::<T>))
}
}
fn get_simple_movement_target<T: SimpleMovementAction>(layer: &mut FsLayer, node_id: NodeId) -> Option<NodeId> {
let view = &layer.tree.view;
view.get(node_id).and_then(|node| T::get_target(view, &node))
fn get_simple_movement_target<T: SimpleMovementAction>(tree: &mut FsTree, node_id: NodeId) -> Option<NodeId> {
tree.view.get(node_id).and_then(|node| T::get_target(&tree.view, &node))
}

View File

@ -37,7 +37,7 @@ impl<A: SimpleMovementAction + 'static, C: MovementCount> MovementWithCountFacto
impl<A: SimpleMovementAction, C: MovementCount> MovementAction for MovementWithCount<A, C> {
fn get_target(&self, layer: &mut FsLayer, environment: &Environment) -> Option<NodeId> where Self: Sized {
let count = self.0.get_count(layer.registers.count, environment);
Some(perform_movement_with_count(layer, Some(count), get_simple_movement_target::<A>))
Some(perform_movement_with_count(&mut layer.tree, Some(count), get_simple_movement_target::<A>))
}
}

View File

@ -5,6 +5,7 @@ use slab_tree::NodeId;
use crate::component::dialog::message::MessageDialogLayer;
use crate::component::filesystem::FsLayer;
use crate::component::filesystem::tree::FsTree;
use crate::state::action::{Action, ActionResult};
use crate::state::Environment;
@ -19,19 +20,19 @@ impl Action<FsLayer> for ExpandCollapse {
return ActionResult::Nothing;
}
if layer.expand_or_collapse(layer.selected_view_node_id) {
if layer.tree.expand_or_collapse(layer.tree.selected_view_node_id) {
if depth > 1 {
if let Some(node) = layer.selected_node() {
if let Some(node) = layer.tree.selected_node() {
if node.data().is_expanded() {
let child_node_ids = node.children().map(|node| node.node_id()).collect();
let remaining_depth = depth.saturating_sub(1);
if !expand_children_to_depth(layer, child_node_ids, remaining_depth) {
return ActionResult::PushLayer(Box::new(MessageDialogLayer::build()
if !expand_children_to_depth(&mut layer.tree, child_node_ids, remaining_depth) {
return ActionResult::push_layer(MessageDialogLayer::build()
.y(layer.dialog_y())
.color(Color::LightYellow)
.title("Expansion Stopped")
.message(format!("Expansion was taking more than {} seconds, stopping now.", MAX_EXPANSION_TIME.as_secs()))
.ok()));
.ok());
}
}
}
@ -46,7 +47,7 @@ impl Action<FsLayer> for ExpandCollapse {
const MAX_EXPANSION_TIME: Duration = Duration::from_secs(10);
fn expand_children_to_depth(layer: &mut FsLayer, mut child_node_ids: Vec<NodeId>, max_depth: usize) -> bool {
fn expand_children_to_depth(tree: &mut FsTree, mut child_node_ids: Vec<NodeId>, max_depth: usize) -> bool {
let start_time = Instant::now();
let mut current_pass_node_ids = Vec::new();
@ -55,8 +56,8 @@ fn expand_children_to_depth(layer: &mut FsLayer, mut child_node_ids: Vec<NodeId>
for node_id in &current_pass_node_ids {
let node_id = *node_id;
layer.tree.expand(node_id);
get_child_node_ids(layer, node_id, &mut child_node_ids);
tree.expand(node_id);
get_child_node_ids(tree, node_id, &mut child_node_ids);
if start_time.elapsed() >= MAX_EXPANSION_TIME {
return false;
@ -67,8 +68,8 @@ fn expand_children_to_depth(layer: &mut FsLayer, mut child_node_ids: Vec<NodeId>
true
}
fn get_child_node_ids(layer: &FsLayer, node_id: NodeId, output_node_ids: &mut Vec<NodeId>) {
if let Some(node) = layer.tree.view.get(node_id) {
fn get_child_node_ids(tree: &FsTree, node_id: NodeId, output_node_ids: &mut Vec<NodeId>) {
if let Some(node) = tree.get_view_node(node_id) {
for child in node.children() {
output_node_ids.push(child.node_id());
}

View File

@ -6,7 +6,7 @@ pub struct RefreshChildrenOfSelected;
impl Action<FsLayer> for RefreshChildrenOfSelected {
fn perform(&self, layer: &mut FsLayer, _environment: &Environment) -> ActionResult {
if layer.refresh_children(layer.selected_view_node_id) {
if layer.tree.refresh_children(layer.tree.selected_view_node_id) {
ActionResult::Draw
} else {
ActionResult::Nothing

View File

@ -1,62 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use slab_tree::NodeId;
use crate::component::filesystem::FsLayer;
pub type FsLayerPendingEvents = Rc<RefCell<Vec<FsLayerEvent>>>;
pub enum FsLayerEvent {
RefreshViewNodeChildren(NodeId),
SelectViewNodeChildByFileName(NodeId, String),
DeleteViewNode(NodeId),
}
impl FsLayerEvent {
pub fn enqueue(self, pending_events: &FsLayerPendingEvents) -> bool {
if let Ok(mut pending_events) = pending_events.try_borrow_mut() {
pending_events.push(self);
true
} else {
false
}
}
pub fn handle(self, layer: &mut FsLayer) {
match self {
Self::RefreshViewNodeChildren(view_node_id) => handle_refresh_view_node_children(layer, view_node_id),
Self::SelectViewNodeChildByFileName(parent_view_node_id, child_file_name) => handle_select_view_node_child_by_name(layer, parent_view_node_id, child_file_name.as_str()),
Self::DeleteViewNode(view_node_id) => handle_delete_view_node(layer, view_node_id),
}
}
}
fn handle_refresh_view_node_children(layer: &mut FsLayer, view_node_id: NodeId) {
layer.refresh_children(view_node_id);
}
fn handle_select_view_node_child_by_name(layer: &mut FsLayer, parent_view_node_id: NodeId, child_file_name: &str) {
layer.tree.expand(parent_view_node_id);
if let Some(parent_node) = layer.tree.view.get(parent_view_node_id) {
for child_node in parent_node.children() {
if layer.tree.get_entry(&child_node).is_some_and(|entry| entry.name().str() == child_file_name) {
layer.selected_view_node_id = child_node.node_id();
return;
}
}
}
}
fn handle_delete_view_node(layer: &mut FsLayer, view_node_id: NodeId) {
let view = &mut layer.tree.view;
if layer.selected_view_node_id == view_node_id {
layer.selected_view_node_id = view.get_node_below_id(view_node_id).or_else(|| view.get_node_above_id(view_node_id)).unwrap_or_else(|| view.root_id());
}
if let Some(view_node) = view.remove(view_node_id) {
layer.tree.model.remove(view_node.model_node_id());
}
}

View File

@ -1,32 +1,27 @@
use std::cell::RefCell;
use std::path::Path;
use std::rc::Rc;
use slab_tree::{NodeId, NodeRef};
use crate::component::filesystem::event::FsLayerPendingEvents;
use crate::component::filesystem::registers::FsTreeRegisters;
use crate::component::filesystem::tree::{FsTree, FsTreeViewNode};
use crate::component::filesystem::tree::FsTree;
use crate::file::FileOwnerNameCache;
use crate::input::keymap::{KeyBinding, KeyMapLookupResult};
use crate::state::action::ActionResult;
use crate::state::Environment;
use crate::state::event::{EventQueue, EventResult};
use crate::state::layer::Layer;
use crate::state::view::Frame;
mod action;
mod event;
mod render;
mod tree;
mod registers;
pub struct FsLayer {
pub tree: FsTree,
pub selected_view_node_id: NodeId,
tree_structure_version: u32,
pub registers: FsTreeRegisters,
cursor_y: u16,
pending_keys: Vec<KeyBinding>,
pending_events: FsLayerPendingEvents,
event_queue: EventQueue<FsLayer>,
file_owner_name_cache: FileOwnerNameCache,
column_width_cache: Option<ColumnWidths>,
}
@ -36,70 +31,25 @@ impl FsLayer {
// Initialize action map early in case it errors.
let _ = *action::ACTION_MAP;
let mut tree = FsTree::with_root_path(root_path);
let root_id = tree.view.root_id();
tree.expand(root_id);
Self {
tree,
selected_view_node_id: root_id,
tree: FsTree::with_root_path(root_path),
tree_structure_version: 0,
cursor_y: 0,
registers: FsTreeRegisters::new(),
pending_keys: Vec::new(),
pending_events: Rc::new(RefCell::new(Vec::new())),
event_queue: EventQueue::new(),
file_owner_name_cache: FileOwnerNameCache::new(),
column_width_cache: None,
}
}
pub fn events(&self) -> EventQueue<Self> {
self.event_queue.rc_clone()
}
pub const fn dialog_y(&self) -> u16 {
self.cursor_y.saturating_add(1)
}
pub fn selected_node(&self) -> Option<NodeRef<FsTreeViewNode>> {
return self.tree.view.get(self.selected_view_node_id);
}
pub fn expand(&mut self, view_node_id: NodeId) -> bool {
let result = self.tree.expand(view_node_id);
tree_structure_changed_if_true(self, result)
}
pub fn collapse(&mut self, view_node_id: NodeId) -> bool {
let result = self.tree.collapse(view_node_id);
tree_structure_changed_if_true(self, result)
}
pub fn expand_or_collapse(&mut self, view_node_id: NodeId) -> bool {
let result = self.tree.expand_or_collapse(view_node_id);
tree_structure_changed_if_true(self, result)
}
pub fn refresh_children(&mut self, view_node_id: NodeId) -> bool {
let result = self.tree.refresh_children(view_node_id);
if result && self.selected_node().is_none() {
self.selected_view_node_id = view_node_id;
}
tree_structure_changed_if_true(self, result)
}
pub fn traverse_up_root(&mut self) -> Option<NodeId> {
let new_root_id = self.tree.traverse_up_root();
tree_structure_changed_if_true(self, new_root_id.is_some());
new_root_id
}
}
fn tree_structure_changed(layer: &mut FsLayer) {
layer.column_width_cache.take();
}
fn tree_structure_changed_if_true(layer: &mut FsLayer, result: bool) -> bool {
if result {
tree_structure_changed(layer);
}
result
}
impl Layer for FsLayer {
@ -133,9 +83,14 @@ impl Layer for FsLayer {
}
}
fn handle_events(&mut self, environment: &Environment) -> EventResult {
self.event_queue.take().into_iter().fold(EventResult::Nothing, |result, event| result.merge(event.dispatch(self, environment)))
}
fn render(&mut self, frame: &mut Frame) {
for event in self.pending_events.take() {
event.handle(self);
if self.tree_structure_version != self.tree.structure_version() {
self.tree_structure_version = self.tree.structure_version();
self.column_width_cache.take();
}
render::render(self, frame);

View File

@ -24,7 +24,7 @@ pub fn render(layer: &mut FsLayer, frame: &mut Frame) {
let column_widths = get_or_update_column_widths(layer, size.width);
let file_owner_name_cache = &mut layer.file_owner_name_cache;
let (rows, cursor_y) = collect_displayed_rows(&layer.tree, layer.selected_view_node_id, size.height as usize);
let (rows, cursor_y) = collect_displayed_rows(&layer.tree, layer.tree.selected_view_node_id, size.height as usize);
layer.cursor_y = cursor_y;
frame.render_widget(Clear, size);
@ -63,7 +63,7 @@ fn collect_displayed_rows(tree: &FsTree, selected_node_id: NodeId, terminal_rows
let mut displayed_rows = Vec::with_capacity(terminal_rows);
let mut cursor_y: u16 = 0;
if let Some(middle_node) = tree.view.get(selected_node_id).or_else(|| tree.view.root()) {
if let Some(middle_node) = tree.selected_node().or_else(|| tree.view.root()) {
let middle_node_id = middle_node.node_id();
displayed_rows.push(NodeRow::from(&middle_node, tree, middle_node_id == selected_node_id));

View File

@ -13,16 +13,39 @@ mod model;
mod view;
pub struct FsTree {
pub model: FsTreeModel,
model: FsTreeModel,
pub view: FsTreeView,
pub selected_view_node_id: NodeId,
structure_version: u32,
}
impl FsTree {
pub fn with_root_path(path: &Path) -> Self {
let model = FsTreeModel::with_root_path(path);
let view = FsTreeView::from_model_root(&model);
let root_id = view.root_id();
Self { model, view }
let mut tree = Self {
model,
view,
selected_view_node_id: root_id,
structure_version: 0,
};
tree.expand(root_id);
tree
}
pub const fn structure_version(&self) -> u32 {
self.structure_version
}
pub fn selected_node(&self) -> Option<NodeRef<FsTreeViewNode>> {
return self.view.get(self.selected_view_node_id);
}
pub fn get_view_node(&self, view_node_id: NodeId) -> Option<NodeRef<FsTreeViewNode>> {
self.view.get(view_node_id)
}
pub fn get_entry(&self, node: &NodeRef<FsTreeViewNode>) -> Option<&FileEntry> {
@ -32,26 +55,79 @@ impl FsTree {
}
pub fn expand(&mut self, view_node_id: NodeId) -> bool {
self.view.expand(view_node_id, &mut self.model)
let result = self.view.expand(view_node_id, &mut self.model);
self.structure_changed_if_true(result)
}
pub fn collapse(&mut self, view_node_id: NodeId) -> bool {
self.view.collapse(view_node_id)
let result = self.view.collapse(view_node_id);
self.structure_changed_if_true(result)
}
pub fn expand_or_collapse(&mut self, view_node_id: NodeId) -> bool {
self.view.expand_or_collapse(view_node_id, &mut self.model)
let result = self.view.expand_or_collapse(view_node_id, &mut self.model);
self.structure_changed_if_true(result)
}
pub fn traverse_up_root(&mut self) -> Option<NodeId> {
self.view.traverse_up_root(&mut self.model)
let new_root_id = self.view.traverse_up_root(&mut self.model);
self.structure_changed_if(new_root_id, Option::is_some)
}
pub fn refresh_children(&mut self, view_node_id: NodeId) -> bool {
if let Some(view_node) = self.view.get(view_node_id) {
self.model.refresh_children(view_node.data().model_node_id()) && self.view.refresh_children(view_node_id, &self.model)
let result = self.model.refresh_children(view_node.data().model_node_id()) && self.view.refresh_children(view_node_id, &self.model);
if result && self.selected_node().is_none() {
self.selected_view_node_id = view_node_id;
}
self.structure_changed_if_true(result)
} else {
false
}
}
pub fn select_child_node_by_name(&mut self, parent_view_node_id: NodeId, child_file_name: &str) -> bool {
self.expand(parent_view_node_id);
if let Some(parent_node) = self.view.get(parent_view_node_id) {
for child_node in parent_node.children() {
if self.get_entry(&child_node).is_some_and(|entry| entry.name().str() == child_file_name) {
self.selected_view_node_id = child_node.node_id();
return true;
}
}
}
false
}
pub fn delete_node(&mut self, view_node_id: NodeId) -> bool {
let view = &mut self.view;
if self.selected_view_node_id == view_node_id {
self.selected_view_node_id = view.get_node_below_id(view_node_id).or_else(|| view.get_node_above_id(view_node_id)).unwrap_or_else(|| view.root_id());
}
if let Some(view_node) = view.remove(view_node_id) {
self.model.remove(view_node.model_node_id());
true
} else {
false
}
}
fn structure_changed(&mut self) {
self.structure_version = self.structure_version.wrapping_add(1);
}
fn structure_changed_if<T, F>(&mut self, result: T, predicate: F) -> T where F: FnOnce(&T) -> bool {
if predicate(&result) {
self.structure_changed();
}
result
}
fn structure_changed_if_true(&mut self, result: bool) -> bool {
self.structure_changed_if(result, |result| *result)
}
}

View File

@ -1,12 +1,16 @@
use std::cmp::min;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::layout::Rect;
use ratatui::style::{Color, Style};
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use crate::component::input::InputField;
use crate::input::keymap::KeyBinding;
use crate::state::action::ActionResult;
use crate::state::Environment;
use crate::state::event::EventResult;
use crate::state::layer::Layer;
use crate::state::view::Frame;
@ -17,12 +21,10 @@ pub struct InputFieldOverlayLayer<'a> {
}
impl<'a> InputFieldOverlayLayer<'a> {
pub fn new(read_only_prefix: &'a str, confirm_action: Box<dyn Fn(String) -> ActionResult>) -> Self {
Self {
field: InputField::new(),
read_only_prefix,
confirm_action,
}
pub fn new<F>(read_only_prefix: &'a str, confirm_action: F) -> Self where F: Fn(String) -> ActionResult + 'static {
let field = InputField::new();
let confirm_action = Box::new(confirm_action);
Self { field, read_only_prefix, confirm_action }
}
}
@ -53,6 +55,10 @@ impl<'a> Layer for InputFieldOverlayLayer<'a> {
}
}
fn handle_events(&mut self, _environment: &Environment) -> EventResult {
EventResult::Nothing
}
fn render(&mut self, frame: &mut Frame) {
let size = frame.size();
if size.width < 1 || size.height < 1 {
@ -62,17 +68,22 @@ impl<'a> Layer for InputFieldOverlayLayer<'a> {
let x = size.x;
let y = size.bottom().saturating_sub(1);
let prefix_style = Style::new()
.fg(Color::Black)
.bg(Color::LightYellow);
let prefix_text = Span::from(self.read_only_prefix);
let prefix_width = min(u16::try_from(prefix_text.width()).unwrap_or(u16::MAX), size.width.saturating_sub(2));
let prefix_paragraph = Paragraph::new(self.read_only_prefix)
.style(prefix_style);
if prefix_width > 0 {
let prefix_style = Style::new()
.fg(Color::Black)
.bg(Color::LightYellow);
let prefix_paragraph = Paragraph::new(self.read_only_prefix)
.style(prefix_style);
frame.render_widget(prefix_paragraph, Rect { x, y, width: prefix_width, height: 1 });
}
frame.render_widget(prefix_paragraph, Rect { x, y, width: 1, height: 1 });
if size.width > 1 {
self.field.render(frame, x.saturating_add(1), y, size.width.saturating_sub(1), Color::LightYellow, Color::Yellow);
if size.width > prefix_width {
self.field.render(frame, x.saturating_add(prefix_width), y, size.width.saturating_sub(prefix_width), Color::LightYellow, Color::Yellow);
}
}
}

View File

@ -22,4 +22,12 @@ impl ActionResult {
Self::Nothing
}
}
pub fn push_layer<T>(layer: T) -> Self where T: Layer + 'static {
Self::PushLayer(Box::new(layer))
}
pub fn replace_layer<T>(layer: T) -> Self where T: Layer + 'static {
Self::ReplaceLayer(Box::new(layer))
}
}

72
src/state/event.rs Normal file
View File

@ -0,0 +1,72 @@
use std::cell::RefCell;
use std::rc::Rc;
use crate::state::Environment;
pub trait Event<L> {
fn dispatch(&self, layer: &mut L, environment: &Environment) -> EventResult;
}
impl<L, F> Event<L> for F where F: Fn(& mut L, &Environment) -> EventResult {
fn dispatch(&self, layer: &mut L, environment: &Environment) -> EventResult {
self(layer, environment)
}
}
pub struct EventQueue<L> {
events: Rc<RefCell<Vec<Box<dyn Event<L>>>>>
}
impl<L> EventQueue<L> {
pub fn new() -> Self {
Self { events: Rc::new(RefCell::new(Vec::new())) }
}
pub fn rc_clone(&self) -> Self {
Self { events: Rc::clone(&self.events) }
}
pub fn enqueue<E: Event<L> + 'static>(&self, event: E) -> bool {
if let Ok(mut events) = self.events.try_borrow_mut() {
events.push(Box::new(event));
true
} else {
false
}
}
pub fn enqueue_fn<F>(&self, event: F) -> bool where F: Fn(&mut L, &Environment) -> EventResult + 'static {
self.enqueue(event)
}
pub fn take(&self) -> Vec<Box<dyn Event<L>>> {
self.events.take()
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum EventResult {
Nothing,
Draw,
Redraw,
}
impl EventResult {
pub const fn draw_if(condition: bool) -> Self {
if condition {
Self::Draw
} else {
Self::Nothing
}
}
pub fn merge(self, other: Self) -> Self {
if self == Self::Redraw || other == Self::Redraw {
Self::Redraw
} else if self == Self::Draw || other == Self::Draw {
Self::Draw
} else {
Self::Nothing
}
}
}

View File

@ -1,9 +1,11 @@
use crate::input::keymap::KeyBinding;
use crate::state::action::ActionResult;
use crate::state::Environment;
use crate::state::event::EventResult;
use crate::state::view::Frame;
pub trait Layer {
fn handle_input(&mut self, environment: &Environment, key_binding: KeyBinding) -> ActionResult;
fn handle_events(&mut self, environment: &Environment) -> EventResult;
fn render(&mut self, frame: &mut Frame);
}

View File

@ -3,6 +3,7 @@ use std::path::Path;
use crate::component::filesystem::FsLayer;
use crate::input::keymap::KeyBinding;
use crate::state::action::ActionResult;
use crate::state::event::EventResult;
use crate::state::layer::Layer;
use crate::state::view::Frame;
@ -10,6 +11,7 @@ pub use self::environment::Environment;
mod environment;
pub mod action;
pub mod event;
pub mod layer;
pub mod view;
@ -26,8 +28,12 @@ impl State {
}
}
pub fn handle_events(&mut self) -> EventResult {
self.layers.iter_mut().fold(EventResult::Nothing, |result, layer| result.merge(layer.handle_events(&self.environment)))
}
pub fn handle_input(&mut self, key_binding: KeyBinding) -> ActionResult {
self.layers.last_mut().map(|layer| layer.handle_input(&self.environment, key_binding)).unwrap_or(ActionResult::Nothing)
self.layers.last_mut().map_or(ActionResult::Nothing, |layer| layer.handle_input(&self.environment, key_binding))
}
pub fn handle_resize(&mut self, width: u16, height: u16) {

View File

@ -10,6 +10,7 @@ use ratatui::widgets::{StatefulWidget, Widget};
pub struct View {
term: Terminal<CrosstermBackend<Stdout>>,
render_request: RenderRequest,
}
impl View {
@ -22,7 +23,7 @@ impl View {
term.hide_cursor()?;
term.clear()?;
Ok(Self { term })
Ok(Self { term, render_request: RenderRequest::Draw })
}
pub fn restore_terminal_on_panic() {
@ -45,11 +46,34 @@ impl View {
self.term.size()
}
pub fn clear(&mut self) -> io::Result<()> {
self.term.clear()
pub fn set_dirty(&mut self, full_redraw: bool) {
let new_request = if full_redraw {
RenderRequest::Redraw
} else {
RenderRequest::Draw
};
self.render_request = self.render_request.merge(new_request);
}
pub fn render<R>(&mut self, renderer: R) -> io::Result<CompletedFrame> where R: FnOnce(&mut Frame) {
pub fn render<R>(&mut self, renderer: R) -> io::Result<()> where R: FnOnce(&mut Frame) {
match self.render_request.consume() {
RenderRequest::Skip => {}
RenderRequest::Draw => {
self.draw(renderer)?;
}
RenderRequest::Redraw => {
self.term.clear()?;
self.draw(renderer)?;
}
}
Ok(())
}
fn draw<R>(&mut self, renderer: R) -> io::Result<CompletedFrame> where R: FnOnce(&mut Frame) {
self.term.draw(|frame| {
let mut frame = Frame::new(frame);
renderer(&mut frame);
@ -58,6 +82,29 @@ impl View {
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum RenderRequest {
Skip,
Draw,
Redraw,
}
impl RenderRequest {
fn merge(self, other: Self) -> Self {
if self == Self::Redraw || other == Self::Redraw {
Self::Redraw
} else if self == Self::Draw || other == Self::Draw {
Self::Draw
} else {
Self::Skip
}
}
fn consume(&mut self) -> Self {
std::mem::replace(self, Self::Skip)
}
}
pub struct Frame<'a, 'b> {
inner: &'a mut ratatui::Frame<'b, CrosstermBackend<Stdout>>,
cursor: Option<(u16, u16)>,