Skip to content

Commit eee942f

Browse files
authored
[wicket] add page up/page down/home/end to the keymap (#3692)
Model this with a new `PendingScroll` action stored on the view. This action is resolved at draw time. This ended up bundling several more changes (sorry!): * Fix an off-by-one issue in the inventory view -- the bottom row of text was getting cut off previously, whoops :( * Fix update pane popup scroll state to be more like the better model @jgallagher created while writing the rack setup screen. Previously, we were storing a single scroll state at the top. This was actually incorrect because when we moved from "waiting" to "failed", we weren't setting the scroll state to Some. The new model stores scroll state inline, and is correct as a result. * Put in a compile-time separation between scrollable and non-scrollable popups -- I made a couple of mistakes with unwrapping scroll popup state which this caught. I tested all the screens which support scrolling, and it worked great on all of them. Closes #3653.
1 parent eb2dc91 commit eee942f

File tree

7 files changed

+572
-556
lines changed

7 files changed

+572
-556
lines changed

wicket/src/keymap.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ pub enum Cmd {
8383
/// Begin rack setup.
8484
StartRackSetup,
8585

86+
/// Page up.
87+
PageUp,
88+
89+
/// Page down.
90+
PageDown,
91+
8692
/// Goto top of list/screen/etc...
8793
GotoTop,
8894

@@ -232,6 +238,10 @@ impl KeyHandler {
232238
KeyCode::Down => Cmd::Down,
233239
KeyCode::Right => Cmd::Right,
234240
KeyCode::Left => Cmd::Left,
241+
KeyCode::PageUp => Cmd::PageUp,
242+
KeyCode::PageDown => Cmd::PageDown,
243+
KeyCode::Home => Cmd::GotoTop,
244+
KeyCode::End => Cmd::GotoBottom,
235245
KeyCode::Char('y') => Cmd::Yes,
236246
KeyCode::Char('u') if event.modifiers == KeyModifiers::CONTROL => {
237247
Cmd::StartUpdate

wicket/src/ui/panes/mod.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod update;
88

99
pub use super::Control;
1010
use crate::ui::defaults::style;
11+
use crate::Cmd;
1112
pub use overview::OverviewPane;
1213
pub use rack_setup::RackSetupPane;
1314
use tui::layout::{Constraint, Direction, Layout, Rect};
@@ -75,6 +76,40 @@ pub fn align_by(
7576
Text::from(Spans::from(text))
7677
}
7778

79+
/// A pending scroll command.
80+
///
81+
/// This is used to communicate between the `on` and `draw` functions.
82+
///
83+
/// **NOTE:** `PendingScroll` deliberately does not implement `Copy` so that
84+
/// users are forced to reset pending scrolls with `Option::take`, or at least
85+
/// that there's some friction.
86+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87+
pub enum PendingScroll {
88+
Up,
89+
Down,
90+
PageUp,
91+
PageDown,
92+
GotoTop,
93+
GotoBottom,
94+
}
95+
96+
impl PendingScroll {
97+
/// Maps a [`Cmd`] to a `PendingScroll`, if possible.
98+
///
99+
/// Use in [`Control::on`] to handle scroll events.
100+
pub fn from_cmd(cmd: &Cmd) -> Option<Self> {
101+
match cmd {
102+
Cmd::Up => Some(Self::Up),
103+
Cmd::Down => Some(Self::Down),
104+
Cmd::PageUp => Some(Self::PageUp),
105+
Cmd::PageDown => Some(Self::PageDown),
106+
Cmd::GotoTop => Some(Self::GotoTop),
107+
Cmd::GotoBottom => Some(Self::GotoBottom),
108+
_ => None,
109+
}
110+
}
111+
}
112+
78113
/// A computed scroll offset.
79114
///
80115
/// This scroll offset is computed by [`Self::new`], and is capped so that we
@@ -107,8 +142,25 @@ impl ComputedScrollOffset {
107142
current_offset: usize,
108143
text_height: usize,
109144
num_lines: usize,
145+
pending_scroll: Option<PendingScroll>,
110146
) -> Self {
111-
let mut offset: usize = current_offset;
147+
let mut offset = if let Some(pending_scroll) = pending_scroll {
148+
// For page up and down, scroll by num_lines - 1 so at least one line is shared.
149+
let page_lines = num_lines.saturating_sub(1);
150+
match pending_scroll {
151+
PendingScroll::Up => current_offset.saturating_sub(1),
152+
PendingScroll::Down => current_offset + 1,
153+
PendingScroll::PageUp => {
154+
current_offset.saturating_sub(page_lines)
155+
}
156+
PendingScroll::PageDown => current_offset + page_lines,
157+
PendingScroll::GotoTop => 0,
158+
// text.height() will get capped below.
159+
PendingScroll::GotoBottom => text_height,
160+
}
161+
} else {
162+
current_offset
163+
};
112164

113165
if offset > text_height {
114166
offset = text_height;

wicket/src/ui/panes/overview.rs

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ use std::collections::BTreeMap;
88
use super::help_text;
99
use super::ComputedScrollOffset;
1010
use super::Control;
11+
use super::PendingScroll;
1112
use crate::state::Component;
1213
use crate::state::{ComponentId, ALL_COMPONENT_IDS};
1314
use crate::ui::defaults::colors::*;
1415
use crate::ui::defaults::style;
1516
use crate::ui::widgets::IgnitionPopup;
16-
use crate::ui::widgets::PopupScrollKind;
1717
use crate::ui::widgets::{BoxConnector, BoxConnectorKind, Rack};
1818
use crate::ui::wrap::wrap_text;
1919
use crate::{Action, Cmd, Frame, State};
@@ -220,6 +220,7 @@ pub struct InventoryView {
220220
help: Vec<(&'static str, &'static str)>,
221221
// Vertical offset used for scrolling
222222
scroll_offsets: BTreeMap<ComponentId, usize>,
223+
pending_scroll: Option<PendingScroll>,
223224
ignition: IgnitionPopup,
224225
popup: Option<PopupKind>,
225226
}
@@ -237,6 +238,7 @@ impl InventoryView {
237238
.iter()
238239
.map(|id| (*id, 0))
239240
.collect(),
241+
pending_scroll: None,
240242
ignition: IgnitionPopup::default(),
241243
popup: None,
242244
}
@@ -267,8 +269,7 @@ impl InventoryView {
267269

268270
let popup_builder =
269271
self.ignition.to_popup_builder(state.rack_state.selected);
270-
// Scrolling in the ignition popup is always disabled.
271-
let popup = popup_builder.build(full_screen, PopupScrollKind::Disabled);
272+
let popup = popup_builder.build(full_screen);
272273
frame.render_widget(popup, full_screen);
273274
}
274275

@@ -319,6 +320,7 @@ impl Control for InventoryView {
319320
.constraints(
320321
[
321322
Constraint::Length(3),
323+
// This includes a top border but not the bottom border.
322324
Constraint::Min(0),
323325
Constraint::Length(3),
324326
]
@@ -366,11 +368,17 @@ impl Control for InventoryView {
366368
),
367369
);
368370

371+
// chunks[1].height includes the top border, which means that the number
372+
// of lines displayed is height - 1.
373+
let num_lines = (chunks[1].height as usize).saturating_sub(1);
374+
369375
let scroll_offset = self.scroll_offsets.get_mut(&component_id).unwrap();
376+
370377
let y_offset = ComputedScrollOffset::new(
371378
*scroll_offset,
372379
text.height(),
373-
chunks[1].height as usize,
380+
num_lines,
381+
self.pending_scroll.take(),
374382
)
375383
.into_offset();
376384
*scroll_offset = y_offset as usize;
@@ -400,6 +408,23 @@ impl Control for InventoryView {
400408
if self.popup.is_some() {
401409
return self.handle_cmd_in_popup(state, cmd);
402410
}
411+
412+
// For Up, Down, PageUp, PageDown, GotoTop and GotoBottom, a
413+
// previous version of this code set the scroll offset directly.
414+
// Sadly that doesn't work with page up/page down because we don't
415+
// know the height of the viewport here.
416+
//
417+
// Instead, we now set a pending_scroll variable that is operated on
418+
// while the view is drawn.
419+
//
420+
// The old scheme does work with the other commands (up, down, goto
421+
// top and goto bottom), but use pending_scroll for all of these
422+
// commands for uniformity.
423+
if let Some(pending_scroll) = PendingScroll::from_cmd(&cmd) {
424+
self.pending_scroll = Some(pending_scroll);
425+
return Some(Action::Redraw);
426+
}
427+
403428
match cmd {
404429
Cmd::Left => {
405430
state.rack_state.prev();
@@ -409,42 +434,6 @@ impl Control for InventoryView {
409434
state.rack_state.next();
410435
Some(Action::Redraw)
411436
}
412-
Cmd::Down => {
413-
let component_id = state.rack_state.selected;
414-
415-
// We currently debug print inventory on each call to
416-
// `draw`, so we don't know how many lines the total text is.
417-
// We also don't know the Rect containing this Control, since
418-
// we don't keep track of them anymore, and only know during
419-
// rendering. Therefore, we just increment and correct
420-
// for more incrments than lines exist during render, since
421-
// `self` is passed mutably.
422-
//
423-
// It's also worth noting that the inventory may update
424-
// before rendering, and so this is a somewhat sensible
425-
// strategy.
426-
*self.scroll_offsets.get_mut(&component_id).unwrap() += 1;
427-
Some(Action::Redraw)
428-
}
429-
Cmd::Up => {
430-
let component_id = state.rack_state.selected;
431-
let offset =
432-
self.scroll_offsets.get_mut(&component_id).unwrap();
433-
*offset = offset.saturating_sub(1);
434-
Some(Action::Redraw)
435-
}
436-
Cmd::GotoTop => {
437-
let component_id = state.rack_state.selected;
438-
*self.scroll_offsets.get_mut(&component_id).unwrap() = 0;
439-
Some(Action::Redraw)
440-
}
441-
Cmd::GotoBottom => {
442-
let component_id = state.rack_state.selected;
443-
// This will get corrected to be the bottom line during `draw`
444-
*self.scroll_offsets.get_mut(&component_id).unwrap() =
445-
usize::MAX;
446-
Some(Action::Redraw)
447-
}
448437
Cmd::Tick => {
449438
// TODO: This only animates when the pane is active. Should we move the
450439
// tick into the [`Runner`] instead?

0 commit comments

Comments
 (0)