use super::*;
pub struct GutterOpts<G> {
pub total_lines: usize,
pub viewport_height: u32,
pub gutter_fn: G,
}
impl<G> GutterOpts<G>
where
G: Fn(usize) -> String,
{
pub fn new(total_lines: usize, viewport_height: u32, gutter_fn: G) -> Self {
Self {
total_lines,
viewport_height,
gutter_fn,
}
}
}
impl GutterOpts<fn(usize) -> String> {
pub fn line_numbers(total_lines: usize, viewport_height: u32) -> Self {
fn label(i: usize) -> String {
format!("{}", i + 1)
}
Self {
total_lines,
viewport_height,
gutter_fn: label,
}
}
}
impl Context {
pub fn scrollable_with_gutter<G, F>(
&mut self,
state: &mut ScrollState,
opts: GutterOpts<G>,
mut f: F,
) -> GutterResponse
where
G: Fn(usize) -> String,
F: FnMut(&mut Context, usize),
{
let GutterOpts {
total_lines,
viewport_height,
gutter_fn,
} = opts;
state.set_bounds(total_lines as u32, viewport_height);
let max_offset = total_lines.saturating_sub(viewport_height as usize);
state.offset = state.offset.min(max_offset);
let next_id = self.rollback.interaction_count;
if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
self.gutter_consume_wheel(rect, state);
}
let visible_count =
(viewport_height as usize).min(total_lines.saturating_sub(state.offset));
let mut gutter_w = 1usize;
for i in 0..visible_count {
let abs = state.offset + i;
let label = gutter_fn(abs);
let w = UnicodeWidthStr::width(label.as_str());
if w > gutter_w {
gutter_w = w;
}
}
let highlights: Vec<HighlightRange> = state.highlights().to_vec();
let current = state.current_highlight();
let theme = self.theme;
let response = self.row(|ui| {
let _ = ui.container().w(gutter_w as u32 + 1).col(|ui| {
for i in 0..visible_count {
let abs = state.offset + i;
let label = gutter_fn(abs);
let label_w = UnicodeWidthStr::width(label.as_str());
let pad = gutter_w.saturating_sub(label_w);
let mut padded = String::with_capacity(label.len() + pad + 1);
for _ in 0..pad {
padded.push(' ');
}
padded.push_str(&label);
padded.push(' ');
let hit = highlights.iter().enumerate().find(|(_, h)| h.contains(abs));
let style = match hit {
Some((idx, _)) if Some(idx) == current => {
Style::new().fg(theme.bg).bg(theme.accent).bold()
}
Some(_) => Style::new().fg(theme.text).bg(theme.surface_hover),
None => Style::new().fg(theme.text_dim),
};
ui.styled(padded, style);
}
});
let _ = ui.container().grow(1).col(|ui| {
for i in 0..visible_count {
let abs = state.offset + i;
let hit = highlights.iter().enumerate().find(|(_, h)| h.contains(abs));
match hit {
Some((idx, _)) if Some(idx) == current => {
let _ = ui.container().bg(theme.surface_hover).row(|ui| f(ui, abs));
}
Some(_) => {
let _ = ui.container().bg(theme.surface).row(|ui| f(ui, abs));
}
None => {
let _ = ui.row(|ui| f(ui, abs));
}
}
}
});
});
GutterResponse {
response,
current_highlight: current,
total_highlights: highlights.len(),
}
}
fn gutter_consume_wheel(&mut self, rect: Rect, state: &mut ScrollState) {
let mut consumed: Vec<usize> = Vec::new();
let delta = self.scroll_lines_per_event as usize;
for (i, mouse) in self.mouse_events_in_rect(rect) {
match mouse.kind {
MouseKind::ScrollUp => {
state.scroll_up(delta);
consumed.push(i);
}
MouseKind::ScrollDown => {
state.scroll_down(delta);
consumed.push(i);
}
_ => {}
}
}
self.consume_indices(consumed);
}
}