Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ less-intuitive mouse controls are:
| Esc | Cancel dropdown |
| j/Down arrow | Move down |
| k/Up arrow | Move up |
| PageDown | Move down a page |
| PageUp | Move up a page |
| End | Move to last item |
| Home | Move to first item |
| H/Shift+Tab | Select previous tab |
| L/Tab | Select next tab |
| ` (Backtick) | Set volume 0% |
Expand Down
90 changes: 90 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ pub enum Action {
Exit,
MoveUp,
MoveDown,
PageUp,
PageDown,
MoveFirst,
MoveLast,
ToggleMute,
SetRelativeVolume(f32),
SetDefault,
Expand All @@ -72,6 +76,10 @@ impl std::fmt::Display for Action {
Action::SelectTab(tab) => write!(f, "Select {tab} tab"),
Action::MoveUp => write!(f, "Move cursor up"),
Action::MoveDown => write!(f, "Move cursor down"),
Action::PageUp => write!(f, "Move cursor up a page"),
Action::PageDown => write!(f, "Move cursor down a page"),
Action::MoveFirst => write!(f, "Move cursor to first item"),
Action::MoveLast => write!(f, "Move cursor to last item"),
Action::TabLeft => write!(f, "Select previous tab"),
Action::TabRight => write!(f, "Select next tab"),
Action::CloseDropdown => write!(f, "Close menu"),
Expand All @@ -96,6 +104,13 @@ impl std::fmt::Display for Action {
}

impl Action {
// The help menu doesn't track how many lines actually fit on screen (it's
// computed inline at render time from the terminal area), so PageUp/
// PageDown there just jump a fixed number of lines rather than a true
// page - close enough for a scrollable overlay, and avoids threading
// layout information through for a fairly minor case.
const HELP_PAGE_STEP: u16 = 10;

fn format_percentage(vol: f32) -> u16 {
(vol * 100.0).trunc() as u16
}
Expand Down Expand Up @@ -556,6 +571,27 @@ impl Handle for Action {
*help_position = help_position.saturating_sub(1);
return Ok(true);
}
Action::PageDown => {
*help_position =
help_position.saturating_add(Self::HELP_PAGE_STEP);
return Ok(true);
}
Action::PageUp => {
*help_position =
help_position.saturating_sub(Self::HELP_PAGE_STEP);
return Ok(true);
}
Action::MoveLast => {
// Clamped to the real bottom by HelpWidget::render() -
// see the "Fix help_position if we are scrolled beyond
// the bottom of the list" comment there.
*help_position = u16::MAX;
return Ok(true);
}
Action::MoveFirst => {
*help_position = 0;
return Ok(true);
}
Action::ActivateDropdown
| Action::CloseDropdown
| Action::Help => {
Expand Down Expand Up @@ -585,6 +621,18 @@ impl Handle for Action {
Action::MoveUp => {
current_list!(app).up(&app.view);
}
Action::PageDown => {
current_list!(app).page_down(&app.view);
}
Action::PageUp => {
current_list!(app).page_up(&app.view);
}
Action::MoveFirst => {
current_list!(app).first(&app.view);
}
Action::MoveLast => {
current_list!(app).last(&app.view);
}
Action::TabLeft => {
app.current_tab_index = app
.current_tab_index
Expand Down Expand Up @@ -1021,6 +1069,48 @@ mod tests {
assert_eq!(app.help_position, Some(0));
}

#[test]
fn help_page_underflow() {
let wirehose = mock::WirehoseHandle::default();
let mut app = fixture(&wirehose);

assert!(Action::Help.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(0));

assert!(Action::PageUp.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(0));
}

#[test]
fn help_page_up_down() {
let wirehose = mock::WirehoseHandle::default();
let mut app = fixture(&wirehose);

assert!(Action::Help.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(0));

assert!(Action::PageDown.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(Action::HELP_PAGE_STEP));

assert!(Action::PageUp.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(0));
}

#[test]
fn help_first_last() {
let wirehose = mock::WirehoseHandle::default();
let mut app = fixture(&wirehose);

assert!(Action::Help.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(0));

assert!(Action::MoveLast.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(u16::MAX));

assert!(Action::MoveFirst.handle(&mut app).unwrap());
assert_eq!(app.help_position, Some(0));
}

#[test]
fn help_toggle() {
let wirehose = mock::WirehoseHandle::default();
Expand Down
4 changes: 4 additions & 0 deletions src/config/keybinding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ impl Keybinding {
(event(KeyCode::Down), Action::MoveDown),
(event(KeyCode::Char('k')), Action::MoveUp),
(event(KeyCode::Up), Action::MoveUp),
(event(KeyCode::PageDown), Action::PageDown),
(event(KeyCode::PageUp), Action::PageUp),
(event(KeyCode::End), Action::MoveLast),
(event(KeyCode::Home), Action::MoveFirst),
(event(KeyCode::Char('H')), Action::TabLeft),
(event(KeyCode::Char('L')), Action::TabRight),
(
Expand Down
157 changes: 157 additions & 0 deletions src/object_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ pub struct ObjectList {
pub dropdown_state: ListState,
/// Targets
pub targets: Vec<(view::Target, String)>,
/// Number of objects visible at once, as of the last `update()` call.
/// Cached here (rather than threaded into every action handler) since
/// it only changes on terminal resize and `update()` already recomputes
/// it every frame from the current area.
page_size: usize,
}

impl ObjectList {
Expand Down Expand Up @@ -74,6 +79,68 @@ impl ObjectList {
}
}

/// Move the selection down by a page (however many objects are visible
/// at once), or to the last object if fewer than a page remain. Leaves
/// the dropdown selection to select_next() the same as `down()`, since
/// a "page" of dropdown targets isn't a meaningful concept - dropdowns
/// are always small.
pub fn page_down(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_some() {
self.dropdown_state.select_next();
return;
}
let mut new_selected = self.selected;
for _ in 0..self.page_size.max(1) {
match view.next_id(self.list_kind, new_selected) {
Some(id) => new_selected = Some(id),
None => break,
}
}
if new_selected.is_some() {
self.select(new_selected);
}
}

/// Move the selection up by a page. See `page_down()`.
pub fn page_up(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_some() {
self.dropdown_state.select_previous();
return;
}
let mut new_selected = self.selected;
for _ in 0..self.page_size.max(1) {
match view.previous_id(self.list_kind, new_selected) {
Some(id) => new_selected = Some(id),
None => break,
}
}
if new_selected.is_some() {
self.select(new_selected);
}
}

/// Jump the selection straight to the first object in the list.
pub fn first(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_some() {
self.dropdown_state.select_first();
return;
}
if let Some(&id) = view.object_ids(self.list_kind).first() {
self.select(Some(id));
}
}

/// Jump the selection straight to the last object in the list.
pub fn last(&mut self, view: &view::View) {
if self.dropdown_state.selected().is_some() {
self.dropdown_state.select_last();
return;
}
if let Some(&id) = view.object_ids(self.list_kind).last() {
self.select(Some(id));
}
}

fn dropdown_open(&mut self, view: &view::View) {
let targets = match self.list_kind {
ListKind::Node(_) => self
Expand Down Expand Up @@ -265,6 +332,7 @@ impl ObjectList {
let objects_len = view.len(self.list_kind);

let visible_count = self.visible_count(&area);
self.page_size = visible_count;

// If objects were removed and the viewport is now below the visible
// objects, move the viewport up so that the bottom of the object list
Expand Down Expand Up @@ -691,6 +759,95 @@ mod tests {
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(10)));
}

#[test]
fn object_list_page_down_page_up() {
let (state, wirehose) = init();
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);

let height = NodeWidget::height() + NodeWidget::spacing();
// + 2 for header and footer; 3 items visible at once
let rect = Rect::new(0, 0, 80, height * 3 + 2);
let mut object_list =
ObjectList::new(ListKind::Node(NodeKind::All), None);

// Select first object, and let update() compute page_size (3) from
// the 3-item-tall rect above.
object_list.down(&view);
object_list.update(rect, &view);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));

object_list.page_down(&view);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(4)));

object_list.page_up(&view);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));
}

#[test]
fn object_list_page_down_overflow() {
let (state, wirehose) = init();
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);

let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height * 3 + 2);
let mut object_list =
ObjectList::new(ListKind::Node(NodeKind::All), None);

object_list.down(&view);
object_list.update(rect, &view);

// Page down well past the last of the 10 mock nodes.
for _ in 0..10 {
object_list.page_down(&view);
object_list.update(rect, &view);
}
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(10)));

for _ in 0..10 {
object_list.page_up(&view);
object_list.update(rect, &view);
}
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));
}

#[test]
fn object_list_first_last() {
let (state, wirehose) = init();
let view = View::from(
&wirehose,
&state,
&config::Names::default(),
&Vec::new(),
);

let height = NodeWidget::height() + NodeWidget::spacing();
let rect = Rect::new(0, 0, 80, height * 3 + 2);
let mut object_list =
ObjectList::new(ListKind::Node(NodeKind::All), None);

// Start in the middle of the 10 mock nodes.
object_list.down(&view);
object_list.update(rect, &view);
object_list.page_down(&view);
object_list.update(rect, &view);

object_list.last(&view);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(10)));

object_list.first(&view);
assert_eq!(object_list.selected, Some(ObjectId::from_raw_id(1)));
}

#[test]
fn visible_objects_changes_with_scroll() {
let (state, wirehose) = init();
Expand Down
8 changes: 8 additions & 0 deletions wiremix.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ keybindings = [
# Select the previous item
{ key = { Char = "k" }, action = "MoveUp" },
{ key = "Up", action = "MoveUp" },
# Select the item a page down (however many items are visible at once)
{ key = "PageDown", action = "PageDown" },
# Select the item a page up
{ key = "PageUp", action = "PageUp" },
# Select the last item
{ key = "End", action = "MoveLast" },
# Select the first item
{ key = "Home", action = "MoveFirst" },
# Select the next tab
{ key = { Char = "L" }, action = "TabRight" },
{ key = "Tab", action = "TabRight" },
Expand Down
Loading