Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/app/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ impl App {

pub(crate) fn reload(&mut self, ss: &SyntaxSet, themes: &ThemeSet) -> bool {
self.reset_numkey_state();
self.reset_toc_scroll_mode();
let path = match &self.filepath {
Some(p) => p,
None => return false,
Expand Down Expand Up @@ -129,6 +130,7 @@ impl App {
self.invalidate_theme_preview_cache();
self.store_current_theme_preview_from(&parsed.lines, &parsed.toc);
self.replace_content(parsed);
self.reset_toc_scroll_mode();
true
}

Expand Down
1 change: 1 addition & 0 deletions src/app/goto_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl App {
if let Some(render_index) = self.find_render_index_for_logical(logical) {
self.goto_line.target = Some(render_index);
self.goto_line.error = false;
self.reset_toc_scroll_mode();
let scroll_pos = render_index.saturating_sub(GOTO_LINE_CONTEXT_OFFSET);
self.scroll = scroll_pos.min(self.max_scroll());
} else {
Expand Down
29 changes: 28 additions & 1 deletion src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ mod io_picker;
mod theme_picker;
pub(crate) use theme_picker::ThemePickerState;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TocScrollMode {
Auto,
Manual(usize),
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct StatusCacheKey {
pct: u16,
Expand All @@ -74,6 +80,7 @@ pub(crate) struct StatusCacheKey {
path_flash_active: bool,
code_block_flash_active: bool,
mouse_capture: bool,
toc_scroll_hint_visible: bool,
}

pub(crate) struct AppConfig {
Expand Down Expand Up @@ -113,7 +120,11 @@ pub(crate) struct App {
toc_display_entries: Vec<usize>,
toc_header_line: Line<'static>,
pub(crate) toc_active_idx: Option<usize>,
pub(crate) toc_active_display_idx: Option<usize>,
pub(crate) hovered_toc_idx: Option<usize>,
toc_scroll_mode: TocScrollMode,
toc_scroll_hint_dismissed: bool,
toc_active_pin: Option<usize>,
status_line: Line<'static>,
status_cache_key: Option<StatusCacheKey>,
pub(super) help_open: bool,
Expand Down Expand Up @@ -246,7 +257,11 @@ impl App {
toc_display_entries: Vec::new(),
toc_header_line: toc_header_line(),
toc_active_idx: None,
toc_active_display_idx: None,
hovered_toc_idx: None,
toc_scroll_mode: TocScrollMode::Auto,
toc_scroll_hint_dismissed: false,
toc_active_pin: None,
status_line: Line::default(),
status_cache_key: None,
help_open: false,
Expand Down Expand Up @@ -459,6 +474,9 @@ impl App {
}

pub(crate) fn active_toc_index(&self) -> Option<usize> {
if let Some(pin) = self.toc_active_pin {
return Some(pin);
}
let mut first_visible = None;
let mut active = None;
let mut last_visible = None;
Expand Down Expand Up @@ -514,22 +532,28 @@ impl App {
let mut top_level_index = 0usize;
let mut lines: Vec<Line<'static>> = Vec::new();
let mut entries: Vec<usize> = Vec::new();
let mut active_display_idx = None;
for (idx, entry, display_level) in self.visible_toc_entries() {
let is_active = active_idx == Some(idx);
let line = build_toc_line_with_index(
entry,
display_level,
(display_level == 1).then_some(top_level_index),
active_idx == Some(idx),
is_active,
);
if display_level == 1 {
top_level_index += 1;
}
if is_active {
active_display_idx = Some(entries.len());
}
lines.push(line);
entries.push(idx);
}
debug_assert_eq!(lines.len(), entries.len());
self.toc_display_lines = lines;
self.toc_display_entries = entries;
self.toc_active_display_idx = active_display_idx;
}

pub(crate) fn refresh_status_cache(&mut self, pct: u16) {
Expand Down Expand Up @@ -585,6 +609,7 @@ impl App {
.map(|(_, t)| t.elapsed() < Duration::from_millis(FLASH_DURATION_MS))
.unwrap_or(false),
mouse_capture: self.mouse_capture,
toc_scroll_hint_visible: self.is_toc_scroll_hint_visible(),
};

if self.status_cache_key.as_ref() == Some(&cache_key) {
Expand All @@ -597,6 +622,8 @@ impl App {

pub(crate) fn refresh_static_caches(&mut self) {
self.toc_active_idx = None;
self.toc_active_display_idx = None;
self.toc_active_pin = None;
self.toc_display_lines.clear();
self.toc_display_entries.clear();
self.refresh_toc_cache();
Expand Down
181 changes: 175 additions & 6 deletions src/app/navigation.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::App;
use super::{App, TocScrollMode};

pub(super) enum CycleDirection {
Forward,
Expand Down Expand Up @@ -39,26 +39,31 @@ impl App {

pub(crate) fn scroll_down(&mut self, n: usize) {
self.reset_numkey_state();
self.reset_toc_scroll_mode();
self.scroll = (self.scroll + n).min(self.max_scroll());
}

pub(crate) fn scroll_up(&mut self, n: usize) {
self.reset_numkey_state();
self.reset_toc_scroll_mode();
self.scroll = self.scroll.saturating_sub(n);
}

pub(crate) fn scroll_top(&mut self) {
self.reset_numkey_state();
self.reset_toc_scroll_mode();
self.scroll = 0;
}

pub(crate) fn scroll_bottom(&mut self) {
self.reset_numkey_state();
self.reset_toc_scroll_mode();
self.scroll = self.max_scroll();
}

pub(crate) fn scroll_to(&mut self, position: usize) {
self.reset_numkey_state();
self.reset_toc_scroll_mode();
self.scroll = position.min(self.max_scroll());
}

Expand All @@ -67,6 +72,7 @@ impl App {
if !self.toc_visible {
self.hovered_toc_idx = None;
}
self.reset_toc_scroll_mode();
}

pub(crate) fn toggle_line_numbers(&mut self) {
Expand Down Expand Up @@ -96,11 +102,17 @@ impl App {
}

pub(crate) fn scroll_to_toc_display_line(&mut self, display_idx: usize) {
if let Some(&entry_idx) = self.toc_display_entries.get(display_idx) {
if let Some(entry) = self.toc.get(entry_idx) {
self.scroll_to(entry.line);
}
}
let Some(&entry_idx) = self.toc_display_entries.get(display_idx) else {
return;
};
let Some(entry_line) = self.toc.get(entry_idx).map(|e| e.line) else {
return;
};
let preserved_offset = self.current_toc_offset();
self.reset_numkey_state();
self.scroll = entry_line.min(self.max_scroll());
self.toc_scroll_mode = TocScrollMode::Manual(preserved_offset);
self.toc_active_pin = Some(entry_idx);
}

pub(crate) fn cycle_numkey(&mut self, key: u8) {
Expand All @@ -109,6 +121,8 @@ impl App {
return;
}

self.reset_toc_scroll_mode();

let direction = if self.reverse_mode {
CycleDirection::Backward
} else {
Expand All @@ -129,4 +143,159 @@ impl App {
self.numkey_cycle = Some(NumkeyCycleState { key, position });
self.scroll = self.toc[group[position]].line.min(self.max_scroll());
}

pub(crate) fn toc_half_page_step(&self) -> usize {
self.toc_list_area
.map(|r| (r.height / 2) as usize)
.unwrap_or(10)
.max(1)
}

pub(crate) fn can_scroll_toc(&self) -> bool {
self.is_toc_visible() && self.has_toc()
}

#[cfg(test)]
pub(crate) fn toc_scroll_mode(&self) -> TocScrollMode {
self.toc_scroll_mode
}

#[cfg(test)]
pub(crate) fn is_toc_scroll_hint_dismissed(&self) -> bool {
self.toc_scroll_hint_dismissed
}

pub(crate) fn toc_overflows(&self, list_height: u16) -> bool {
self.toc_display_lines.len() > (list_height as usize).saturating_sub(1)
}

pub(crate) fn is_toc_scroll_hint_visible(&self) -> bool {
if !self.is_toc_visible() || !self.has_toc() || self.toc_scroll_hint_dismissed {
return false;
}
self.toc_list_area
.is_some_and(|area| self.toc_overflows(area.height))
}

fn max_toc_scroll_offset(&self, list_height: u16) -> usize {
(self.toc_display_lines.len() + 1).saturating_sub(list_height as usize)
}

pub(crate) fn toc_scroll_offset(&self, list_height: u16) -> usize {
let max_offset = self.max_toc_scroll_offset(list_height);
let list_height = list_height as usize;
match self.toc_scroll_mode {
TocScrollMode::Manual(offset) => offset.min(max_offset),
TocScrollMode::Auto => {
let Some(active_display_idx) = self.toc_active_display_idx else {
return 0;
};
let mut offset = 0usize;
if active_display_idx + 1 >= list_height {
offset = active_display_idx + 2 - list_height;
}
offset.min(max_offset)
}
}
}

pub(crate) fn reset_toc_scroll_mode(&mut self) {
self.toc_scroll_mode = TocScrollMode::Auto;
self.toc_active_pin = None;
}

pub(crate) fn scroll_toc_down(&mut self, n: usize) {
let current = self.current_toc_offset();
self.set_toc_manual_offset(current.saturating_add(n));
}

pub(crate) fn scroll_toc_up(&mut self, n: usize) {
let current = self.current_toc_offset();
self.set_toc_manual_offset(current.saturating_sub(n));
}

pub(crate) fn focus_next_top_level_toc(&mut self) {
self.cycle_visible_top_level(CycleDirection::Forward);
}

pub(crate) fn focus_prev_top_level_toc(&mut self) {
self.cycle_visible_top_level(CycleDirection::Backward);
}

fn cycle_visible_top_level(&mut self, direction: CycleDirection) {
let visible_tops = self.visible_top_level_display_indices();
if visible_tops.is_empty() {
return;
}

self.dismiss_toc_scroll_hint_if_visible();

let active_position = self.toc_active_display_idx.and_then(|active_idx| {
if !self.is_display_idx_in_toc_window(active_idx) {
return None;
}
visible_tops
.iter()
.rposition(|(d_idx, _)| *d_idx <= active_idx)
});

let target_display_idx = match active_position {
Some(position) => {
let len = visible_tops.len();
let new_position = match direction {
CycleDirection::Forward => (position + 1) % len,
CycleDirection::Backward => (position + len - 1) % len,
};
visible_tops[new_position].0
}
None => match direction {
CycleDirection::Forward => visible_tops.first().unwrap().0,
CycleDirection::Backward => visible_tops.last().unwrap().0,
},
};

self.scroll_to_toc_display_line(target_display_idx);
}

fn visible_top_level_display_indices(&self) -> Vec<(usize, usize)> {
let Some(area) = self.toc_list_area else {
return Vec::new();
};
let offset = self.toc_scroll_offset(area.height);
let list_height = area.height as usize;
self.visible_toc_entries()
.enumerate()
.skip(offset)
.take(list_height)
.filter(|(_, (_, _, dl))| *dl == 1)
.map(|(display_idx, (entry_idx, _, _))| (display_idx, entry_idx))
.collect()
}

fn is_display_idx_in_toc_window(&self, display_idx: usize) -> bool {
let Some(area) = self.toc_list_area else {
return false;
};
let offset = self.toc_scroll_offset(area.height);
(offset..offset + area.height as usize).contains(&display_idx)
}

fn current_toc_offset(&self) -> usize {
let list_height = self.toc_list_area.map(|r| r.height).unwrap_or(0);
self.toc_scroll_offset(list_height)
}

fn set_toc_manual_offset(&mut self, offset: usize) {
self.dismiss_toc_scroll_hint_if_visible();
let list_height = self.toc_list_area.map(|r| r.height).unwrap_or(0);
let max_offset = self.max_toc_scroll_offset(list_height);
self.toc_scroll_mode = TocScrollMode::Manual(offset.min(max_offset));
self.hovered_toc_idx = None;
}

fn dismiss_toc_scroll_hint_if_visible(&mut self) {
if self.is_toc_scroll_hint_visible() {
self.toc_scroll_hint_dismissed = true;
}
}
}
3 changes: 3 additions & 0 deletions src/app/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ impl App {
self.search.matches = search_matches;
self.search.idx = 0;
if let Some(&f) = self.search.matches.first() {
self.reset_toc_scroll_mode();
self.scroll = f.min(self.max_scroll());
}
}
Expand Down Expand Up @@ -164,6 +165,7 @@ impl App {
return;
}
self.reset_numkey_state();
self.reset_toc_scroll_mode();
self.search.idx = (self.search.idx + 1) % self.search.matches.len();
self.scroll = self.search.matches[self.search.idx].min(self.max_scroll());
}
Expand All @@ -173,6 +175,7 @@ impl App {
return;
}
self.reset_numkey_state();
self.reset_toc_scroll_mode();
if self.search.idx == 0 {
self.search.idx = self.search.matches.len() - 1;
} else {
Expand Down
Loading
Loading