Fix TUI details viewport scrolling
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
//! Secret-aware presentation state for one storage-owned entry document.
|
||||
|
||||
use std::fmt;
|
||||
use std::{cell::Cell, fmt};
|
||||
|
||||
use ironstorage::{
|
||||
document::{DocumentError, EntryDocument, EntryField, EntryFieldId, EntrySensitivity},
|
||||
@@ -15,7 +15,7 @@ pub struct EntryViewer {
|
||||
document: EntryDocument,
|
||||
focused: usize,
|
||||
revealed: Option<EntryFieldId>,
|
||||
scroll: usize,
|
||||
scroll: Cell<usize>,
|
||||
}
|
||||
|
||||
impl EntryViewer {
|
||||
@@ -24,7 +24,7 @@ impl EntryViewer {
|
||||
document,
|
||||
focused: 0,
|
||||
revealed: None,
|
||||
scroll: 0,
|
||||
scroll: Cell::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ impl EntryViewer {
|
||||
self.hide_revealed();
|
||||
if !self.document.fields().is_empty() {
|
||||
self.focused = (self.focused + 1) % self.document.fields().len();
|
||||
self.scroll = self.focused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +62,6 @@ impl EntryViewer {
|
||||
.focused
|
||||
.checked_sub(1)
|
||||
.unwrap_or(self.document.fields().len() - 1);
|
||||
self.scroll = self.focused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,23 +88,56 @@ impl EntryViewer {
|
||||
}
|
||||
|
||||
pub fn scroll(&self) -> usize {
|
||||
self.scroll
|
||||
self.scroll.get()
|
||||
}
|
||||
|
||||
/// Keep the focused rendered row range inside the supplied viewport while
|
||||
/// retaining the current offset whenever it is already visible.
|
||||
pub fn ensure_focus_visible(
|
||||
&self,
|
||||
focused_start: usize,
|
||||
focused_height: usize,
|
||||
total_rows: usize,
|
||||
viewport_rows: usize,
|
||||
) -> usize {
|
||||
if self.document.fields().is_empty() || total_rows == 0 {
|
||||
self.scroll.set(0);
|
||||
return 0;
|
||||
}
|
||||
if viewport_rows == 0 {
|
||||
let scroll = self.scroll.get().min(total_rows.saturating_sub(1));
|
||||
self.scroll.set(scroll);
|
||||
return scroll;
|
||||
}
|
||||
let maximum = total_rows.saturating_sub(viewport_rows);
|
||||
let mut scroll = self.scroll.get().min(maximum);
|
||||
let focused_height = focused_height.max(1);
|
||||
let focused_end = focused_start.saturating_add(focused_height);
|
||||
let viewport_end = scroll.saturating_add(viewport_rows);
|
||||
if focused_height > viewport_rows {
|
||||
if focused_end <= scroll {
|
||||
scroll = focused_end.saturating_sub(1);
|
||||
} else if focused_start >= viewport_end {
|
||||
scroll = focused_start;
|
||||
}
|
||||
} else if focused_start < scroll {
|
||||
scroll = focused_start;
|
||||
} else {
|
||||
if focused_end > viewport_end {
|
||||
scroll = focused_end.saturating_sub(viewport_rows);
|
||||
}
|
||||
}
|
||||
scroll = scroll.min(maximum);
|
||||
self.scroll.set(scroll);
|
||||
scroll
|
||||
}
|
||||
|
||||
pub fn scroll_down(&mut self, rows: usize) {
|
||||
self.scroll = self.scroll.saturating_add(rows).min(self.scroll_limit());
|
||||
self.scroll.set(self.scroll.get().saturating_add(rows));
|
||||
}
|
||||
|
||||
pub fn scroll_up(&mut self, rows: usize) {
|
||||
self.scroll = self.scroll.saturating_sub(rows);
|
||||
}
|
||||
|
||||
fn scroll_limit(&self) -> usize {
|
||||
self.document
|
||||
.fields()
|
||||
.iter()
|
||||
.map(|field| field.value().len().saturating_add(1))
|
||||
.sum()
|
||||
self.scroll.set(self.scroll.get().saturating_sub(rows));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +148,7 @@ impl fmt::Debug for EntryViewer {
|
||||
.field("document", &self.document)
|
||||
.field("focused", &self.focused)
|
||||
.field("revealed", &self.revealed.map(|_| "[REDACTED]"))
|
||||
.field("scroll", &self.scroll)
|
||||
.field("scroll", &self.scroll.get())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -201,4 +232,69 @@ mod tests {
|
||||
viewer.scroll_up(usize::MAX);
|
||||
assert_eq!(viewer.scroll(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_only_scrolls_when_it_crosses_a_viewport_edge() {
|
||||
let mut viewer = EntryViewer::new(fixture_document("email/personal"));
|
||||
assert_eq!(viewer.document().fields().len(), 3);
|
||||
|
||||
assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 3), 0);
|
||||
viewer.focus_next();
|
||||
assert_eq!(viewer.ensure_focus_visible(1, 1, 3, 3), 0);
|
||||
viewer.focus_next();
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 3), 0);
|
||||
viewer.focus_next();
|
||||
assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 3), 0);
|
||||
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.focused_index(), Some(2));
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 2), 1);
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.focused_index(), Some(1));
|
||||
assert_eq!(viewer.ensure_focus_visible(1, 1, 3, 2), 1);
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.focused_index(), Some(0));
|
||||
assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 2), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_resize_clamps_then_minimally_reveals_the_focus() {
|
||||
let mut viewer = EntryViewer::new(fixture_document("email/personal"));
|
||||
viewer.focus_next();
|
||||
viewer.focus_next();
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 1), 2);
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 2), 1);
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 3), 0);
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 1, 3, 1), 2);
|
||||
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.ensure_focus_visible(1, 1, 3, 1), 1);
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.ensure_focus_visible(0, 1, 3, 1), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped_fields_use_rendered_row_ranges_for_minimal_scrolling() {
|
||||
let mut viewer = EntryViewer::new(fixture_document("email/personal"));
|
||||
viewer.focus_next();
|
||||
viewer.focus_next();
|
||||
|
||||
assert_eq!(viewer.ensure_focus_visible(4, 2, 6, 4), 2);
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.ensure_focus_visible(2, 2, 6, 4), 2);
|
||||
viewer.focus_previous();
|
||||
assert_eq!(viewer.ensure_focus_visible(0, 2, 6, 4), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_field_taller_than_the_viewport_allows_needed_manual_scrolling() {
|
||||
let mut viewer = EntryViewer::new(fixture_document("email/personal"));
|
||||
viewer.focus_next();
|
||||
|
||||
assert_eq!(viewer.ensure_focus_visible(1, 5, 7, 3), 0);
|
||||
viewer.scroll_down(2);
|
||||
assert_eq!(viewer.ensure_focus_visible(1, 5, 7, 3), 2);
|
||||
viewer.scroll_down(usize::MAX);
|
||||
assert_eq!(viewer.ensure_focus_visible(1, 5, 7, 3), 4);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user