use super::{App, TocScrollMode};
pub(super) enum CycleDirection {
Forward,
Backward,
}
pub(super) struct NumkeyCycleState {
pub(super) key: u8,
pub(super) position: usize,
}
impl App {
pub(crate) fn max_scroll(&self) -> usize {
self.total()
.saturating_sub(self.content_area.height as usize)
}
pub(crate) fn visible_end(&self) -> usize {
(self.scroll + self.content_area.height as usize).min(self.total())
}
pub(crate) fn scroll_percent(&self) -> u16 {
let max = self.max_scroll();
if max == 0 {
return 100;
}
((self.scroll * 100) / max).min(100) as u16
}
pub(super) fn reset_numkey_state(&mut self) {
self.numkey_cycle = None;
self.reverse_mode = false;
}
pub(crate) fn toggle_reverse_mode(&mut self) {
self.reverse_mode = !self.reverse_mode;
}
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());
}
pub(crate) fn toggle_toc(&mut self) {
self.toc_visible = !self.toc_visible;
if !self.toc_visible {
self.hovered_toc_idx = None;
}
self.reset_toc_scroll_mode();
}
pub(crate) fn toggle_line_numbers(&mut self) {
self.line_number_visible = !self.line_number_visible;
}
fn toc_group_for_numkey(&self, key: u8) -> Vec<usize> {
let mut group = Vec::new();
let mut top_level_index = 0u8;
let mut collecting = false;
for (idx, _entry, display_level) in self.visible_toc_entries() {
if display_level == 1 {
if collecting {
break;
}
top_level_index += 1;
if top_level_index == key {
collecting = true;
group.push(idx);
}
} else if collecting {
group.push(idx);
}
}
group
}
pub(crate) fn scroll_to_toc_display_line(&mut self, display_idx: usize) {
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) {
let group = self.toc_group_for_numkey(key);
if group.is_empty() {
return;
}
self.reset_toc_scroll_mode();
let direction = if self.reverse_mode {
CycleDirection::Backward
} else {
CycleDirection::Forward
};
let position = match self.numkey_cycle.as_ref().filter(|s| s.key == key) {
Some(state) => match direction {
CycleDirection::Forward => (state.position + 1) % group.len(),
CycleDirection::Backward => (state.position + group.len() - 1) % group.len(),
},
None => {
self.reverse_mode = false;
0
}
};
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))
}
pub(super) 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);
self.toc_scroll_mode.offset().min(max_offset)
}
pub(crate) fn reset_toc_scroll_mode(&mut self) {
self.toc_scroll_mode = TocScrollMode::Auto(self.toc_scroll_mode.offset());
self.toc_active_pin = None;
}
pub(crate) fn clear_toc_scroll_state(&mut self) {
self.toc_scroll_mode = TocScrollMode::Auto(0);
self.toc_active_pin = None;
self.toc_scroll_hint_dismissed = false;
}
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;
}
}
}