feat(core): Add input modes and half-page cursor movement.
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
use crate::error::PiklError;
|
||||
use crate::event::{Action, MenuEvent, MenuResult, ViewState, VisibleItem};
|
||||
use crate::event::{Action, MenuEvent, MenuResult, Mode, ViewState, VisibleItem};
|
||||
use crate::model::traits::Menu;
|
||||
use crate::navigation::Viewport;
|
||||
use serde_json::Value;
|
||||
@@ -35,6 +35,7 @@ pub struct MenuRunner<M: Menu> {
|
||||
menu: M,
|
||||
viewport: Viewport,
|
||||
filter_text: Arc<str>,
|
||||
mode: Mode,
|
||||
action_rx: mpsc::Receiver<Action>,
|
||||
event_tx: broadcast::Sender<MenuEvent>,
|
||||
}
|
||||
@@ -55,6 +56,7 @@ impl<M: Menu> MenuRunner<M> {
|
||||
menu,
|
||||
viewport: Viewport::new(),
|
||||
filter_text: Arc::from(""),
|
||||
mode: Mode::default(),
|
||||
action_rx,
|
||||
event_tx,
|
||||
};
|
||||
@@ -100,6 +102,7 @@ impl<M: Menu> MenuRunner<M> {
|
||||
filter_text: Arc::clone(&self.filter_text),
|
||||
total_items: self.menu.total(),
|
||||
total_filtered: self.menu.filtered_count(),
|
||||
mode: self.mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +161,18 @@ impl<M: Menu> MenuRunner<M> {
|
||||
self.viewport.set_height(height as usize);
|
||||
ActionOutcome::Broadcast
|
||||
}
|
||||
Action::HalfPageUp(n) => {
|
||||
self.viewport.half_page_up(n);
|
||||
ActionOutcome::Broadcast
|
||||
}
|
||||
Action::HalfPageDown(n) => {
|
||||
self.viewport.half_page_down(n);
|
||||
ActionOutcome::Broadcast
|
||||
}
|
||||
Action::SetMode(m) => {
|
||||
self.mode = m;
|
||||
ActionOutcome::Broadcast
|
||||
}
|
||||
Action::AddItems(values) => {
|
||||
self.menu.add_raw(values);
|
||||
self.run_filter();
|
||||
@@ -166,6 +181,12 @@ impl<M: Menu> MenuRunner<M> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the initial mode before running the event loop.
|
||||
/// Used by `--start-mode` CLI flag.
|
||||
pub fn set_initial_mode(&mut self, mode: Mode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
/// Run the menu event loop. Consumes actions and
|
||||
/// broadcasts events.
|
||||
///
|
||||
@@ -617,6 +638,73 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
// -- New action variant tests --
|
||||
|
||||
#[test]
|
||||
fn apply_half_page_down() {
|
||||
let (mut m, _tx) = test_menu();
|
||||
m.run_filter();
|
||||
m.apply_action(Action::Resize { height: 4 });
|
||||
let outcome = m.apply_action(Action::HalfPageDown(1));
|
||||
assert!(matches!(outcome, ActionOutcome::Broadcast));
|
||||
assert_eq!(m.viewport.cursor(), 2); // 4/2 = 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_half_page_up() {
|
||||
let (mut m, _tx) = test_menu();
|
||||
m.run_filter();
|
||||
m.apply_action(Action::Resize { height: 4 });
|
||||
m.apply_action(Action::HalfPageDown(1));
|
||||
let outcome = m.apply_action(Action::HalfPageUp(1));
|
||||
assert!(matches!(outcome, ActionOutcome::Broadcast));
|
||||
assert_eq!(m.viewport.cursor(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_set_mode_normal() {
|
||||
let mut m = ready_menu();
|
||||
let outcome = m.apply_action(Action::SetMode(Mode::Normal));
|
||||
assert!(matches!(outcome, ActionOutcome::Broadcast));
|
||||
assert_eq!(m.build_view_state().mode, Mode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_set_mode_insert() {
|
||||
let mut m = ready_menu();
|
||||
m.apply_action(Action::SetMode(Mode::Normal));
|
||||
let outcome = m.apply_action(Action::SetMode(Mode::Insert));
|
||||
assert!(matches!(outcome, ActionOutcome::Broadcast));
|
||||
assert_eq!(m.build_view_state().mode, Mode::Insert);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_set_mode_preserves_filter() {
|
||||
let mut m = ready_menu();
|
||||
m.apply_action(Action::UpdateFilter("al".to_string()));
|
||||
let count_before = m.menu.filtered_count();
|
||||
let filter_before = m.filter_text.clone();
|
||||
m.apply_action(Action::SetMode(Mode::Normal));
|
||||
assert_eq!(&*m.filter_text, &*filter_before);
|
||||
assert_eq!(m.menu.filtered_count(), count_before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_initial_mode_works() {
|
||||
let (mut m, _tx) = test_menu();
|
||||
m.set_initial_mode(Mode::Normal);
|
||||
m.run_filter();
|
||||
assert_eq!(m.build_view_state().mode, Mode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_state_mode_after_switch() {
|
||||
let mut m = ready_menu();
|
||||
m.apply_action(Action::SetMode(Mode::Normal));
|
||||
let vs = m.build_view_state();
|
||||
assert_eq!(vs.mode, Mode::Normal);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actions_process_in_order_add_items_then_filter_then_confirm() {
|
||||
// AddItems + filter + confirm, all back-to-back.
|
||||
|
||||
Reference in New Issue
Block a user