W46: Animated cursor movement trail
When the user moves the cursor (up/down/home/end), the previous cursor position briefly shows a thin accent-coloured bar (▏) to the left of the row. Creates a subtle trail effect that makes the new cursor position more obvious. - Panel::prev_cursor: usize field set on every move - Panel::cursor_flash: Option<Instant> tracks the flash - Panel::cursor_flash_progress() returns Some(frac) for ~200ms after each move - cursor_up / cursor_down / cursor_home / cursor_end set the previous position and trigger the flash - Render path draws the bar at the previous row in accent colour Added 1 unit test verifying prev_cursor and cursor_flash on move. Tests: 1406 pass (was 1405), zero warnings.
This commit is contained in:
@@ -115,6 +115,12 @@ pub struct Panel {
|
||||
entries: Vec<Entry>,
|
||||
/// Cursor position (index into `entries`). 0 is the ".." entry.
|
||||
cursor: usize,
|
||||
/// Previous cursor position (set when the cursor moves). Used by
|
||||
/// the renderer to draw a brief trail behind the active cursor.
|
||||
prev_cursor: usize,
|
||||
/// `Some(timestamp)` while the cursor-trail flash is active.
|
||||
/// `None` when no flash is active.
|
||||
cursor_flash: Option<std::time::Instant>,
|
||||
/// Top-line scroll position.
|
||||
top: usize,
|
||||
/// Files marked with Insert/Space.
|
||||
@@ -164,6 +170,8 @@ impl Panel {
|
||||
path: PathBuf::new(),
|
||||
entries: Vec::new(),
|
||||
cursor: 0,
|
||||
prev_cursor: 0,
|
||||
cursor_flash: None,
|
||||
top: 0,
|
||||
marked: HashSet::new(),
|
||||
sort_field: SortField::from_config(&cfg.sort_field),
|
||||
@@ -239,6 +247,12 @@ impl Panel {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
/// Previous cursor index (set on every move; reset by render).
|
||||
#[must_use]
|
||||
pub fn prev_cursor(&self) -> usize {
|
||||
self.prev_cursor
|
||||
}
|
||||
|
||||
/// Top-line scroll index.
|
||||
#[must_use]
|
||||
pub fn top(&self) -> usize {
|
||||
@@ -497,6 +511,20 @@ impl Panel {
|
||||
v
|
||||
}
|
||||
|
||||
/// `Some(frac)` while the cursor-trail flash is active, where
|
||||
/// `1.0` = just after move and `0.0` = fully expired. `None`
|
||||
/// when no flash is active.
|
||||
pub fn cursor_flash_progress(&self) -> Option<f32> {
|
||||
let start = self.cursor_flash?;
|
||||
const FLASH_MS: f32 = 200.0;
|
||||
let elapsed_ms = start.elapsed().as_secs_f32() * 1000.0;
|
||||
if elapsed_ms >= FLASH_MS {
|
||||
return None;
|
||||
}
|
||||
let remaining = FLASH_MS - elapsed_ms;
|
||||
Some((remaining / FLASH_MS).clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
/// Snapshot of file-system state for every non-`..` entry, in
|
||||
/// display order. The returned vector preserves the panel's
|
||||
/// current sort order so callers can build a `name -> size` map
|
||||
@@ -586,25 +614,37 @@ impl Panel {
|
||||
/// Move the cursor up by one line.
|
||||
pub fn cursor_up(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
self.prev_cursor = self.cursor;
|
||||
self.cursor -= 1;
|
||||
self.cursor_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the cursor down by one line.
|
||||
pub fn cursor_down(&mut self) {
|
||||
if self.cursor + 1 < self.entries.len() {
|
||||
self.prev_cursor = self.cursor;
|
||||
self.cursor += 1;
|
||||
self.cursor_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the cursor to the top of the list.
|
||||
pub fn cursor_home(&mut self) {
|
||||
if self.cursor != 0 {
|
||||
self.prev_cursor = self.cursor;
|
||||
self.cursor_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
self.cursor = 0;
|
||||
}
|
||||
|
||||
/// Move the cursor to the bottom of the list.
|
||||
pub fn cursor_end(&mut self) {
|
||||
if !self.entries.is_empty() {
|
||||
if self.cursor != self.entries.len() - 1 {
|
||||
self.prev_cursor = self.cursor;
|
||||
self.cursor_flash = Some(std::time::Instant::now());
|
||||
}
|
||||
self.cursor = self.entries.len() - 1;
|
||||
}
|
||||
}
|
||||
@@ -1243,6 +1283,25 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_movement_records_prev_cursor_and_triggers_flash() {
|
||||
let dir = std::env::temp_dir().join("tlc-panel-cursor-trail");
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
fs::write(dir.join("a.txt"), b"x").unwrap();
|
||||
fs::write(dir.join("b.txt"), b"x").unwrap();
|
||||
let mut p = Panel::new(&dir, &empty_cfg()).unwrap();
|
||||
// Initially no flash, prev_cursor == cursor.
|
||||
assert!(p.cursor_flash_progress().is_none());
|
||||
// cursor_down sets prev_cursor and triggers the flash.
|
||||
p.cursor_down();
|
||||
assert!(p.cursor_flash_progress().is_some());
|
||||
// prev_cursor holds the index before the move.
|
||||
// The exact value depends on which entry was at cursor 0.
|
||||
assert_ne!(p.prev_cursor(), p.cursor());
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_match_len_returns_filter_prefix() {
|
||||
let dir = std::env::temp_dir().join("tlc-panel-filter-match-len");
|
||||
|
||||
@@ -449,6 +449,26 @@ impl FileManager {
|
||||
.style(Style::default().fg(panel_fg).bg(panel_bg));
|
||||
frame.render_widget(list, list_area);
|
||||
|
||||
// Cursor-trail: briefly highlight the previous
|
||||
// cursor row with a thin accent-coloured bar.
|
||||
if let Some(frac) = panel.cursor_flash_progress() {
|
||||
let prev_visible = (panel.prev_cursor() as i64) - (top as i64);
|
||||
if prev_visible >= 0 && (prev_visible as u16) < body_height {
|
||||
let prev_y = body_y + prev_visible as u16;
|
||||
let bar = Paragraph::new(Span::styled(
|
||||
"▏",
|
||||
Style::default()
|
||||
.fg(self.theme.accent)
|
||||
.bg(panel_bg),
|
||||
));
|
||||
frame.render_widget(
|
||||
bar,
|
||||
Rect::new(inner.x, prev_y, 1, 1),
|
||||
);
|
||||
let _ = frac; // currently unused; reserved for future fade
|
||||
}
|
||||
}
|
||||
|
||||
let total = panel.entry_count().max(1);
|
||||
let mut sb_state = ScrollbarState::new(total)
|
||||
.viewport_content_length(body_height as usize)
|
||||
|
||||
Reference in New Issue
Block a user