Acord/viewport/src/minimap.rs

189 lines
5.3 KiB
Rust

use iced_wgpu::core::{
mouse, Color, Element, Length, Point, Rectangle, Size, Theme,
};
use iced_widget::canvas::{self, Frame};
use crate::palette;
#[derive(Clone, Copy, Debug)]
pub enum LineKind {
Empty,
Plain,
Heading,
Code,
List,
Quote,
}
#[derive(Clone, Debug)]
pub struct MinimapLine {
pub width_chars: u16,
pub kind: LineKind,
}
/// classifies a single source line for minimap colouring.
pub fn classify(line: &str) -> MinimapLine {
let trimmed = line.trim_start();
let kind = if trimmed.is_empty() {
LineKind::Empty
} else if trimmed.starts_with('#') {
LineKind::Heading
} else if trimmed.starts_with("```") || trimmed.starts_with(" ") || trimmed.starts_with('\t') {
LineKind::Code
} else if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
LineKind::List
} else if trimmed.starts_with("> ") {
LineKind::Quote
} else {
LineKind::Plain
};
let width = line.chars().count().min(u16::MAX as usize) as u16;
MinimapLine { width_chars: width, kind }
}
/// turns the document text into per-line minimap data in one pass.
pub fn classify_text(text: &str) -> Vec<MinimapLine> {
text.lines().map(classify).collect()
}
#[derive(Clone)]
pub struct MinimapData {
pub lines: Vec<MinimapLine>,
pub viewport_first: usize,
pub viewport_last: usize,
pub hovered: bool,
pub suppressed: bool,
}
struct MinimapProgram<M, F>
where
M: Clone + 'static,
F: Fn(f32) -> M,
{
data: MinimapData,
on_jump: F,
_marker: std::marker::PhantomData<fn() -> M>,
}
impl<M, F> canvas::Program<M, Theme, iced_wgpu::Renderer> for MinimapProgram<M, F>
where
M: Clone + 'static,
F: Fn(f32) -> M,
{
type State = ();
fn draw(
&self,
_state: &(),
renderer: &iced_wgpu::Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry<iced_wgpu::Renderer>> {
let mut frame = Frame::new(renderer, bounds.size());
let p = palette::current();
let total = self.data.lines.len().max(1) as f32;
let h = bounds.height;
let pixels_per_line = (h / total).max(0.5);
let bar_h = pixels_per_line.max(1.0);
let alpha = if self.data.suppressed {
0.0
} else if self.data.hovered {
0.55
} else {
0.18
};
if alpha == 0.0 {
return vec![frame.into_geometry()];
}
let max_chars = self.data.lines.iter().map(|l| l.width_chars as f32).fold(1.0, f32::max);
for (i, line) in self.data.lines.iter().enumerate() {
let y = (i as f32 / total) * h;
let bar_w = (line.width_chars as f32 / max_chars) * bounds.width * 0.85;
let color = match line.kind {
LineKind::Empty => continue,
LineKind::Heading => Color { a: alpha + 0.20, ..p.mauve },
LineKind::Code => Color { a: alpha + 0.05, ..p.peach },
LineKind::List => Color { a: alpha + 0.05, ..p.green },
LineKind::Quote => Color { a: alpha + 0.05, ..p.teal },
LineKind::Plain => Color { a: alpha, ..p.text },
};
frame.fill_rectangle(
Point::new(bounds.width * 0.075, y),
Size::new(bar_w, bar_h),
color,
);
}
if self.data.viewport_last > self.data.viewport_first {
let top = (self.data.viewport_first as f32 / total) * h;
let bot = ((self.data.viewport_last as f32) / total) * h;
let height = (bot - top).max(8.0);
let indicator_alpha = if self.data.hovered { 0.22 } else { 0.12 };
frame.fill_rectangle(
Point::new(0.0, top),
Size::new(bounds.width, height),
Color { a: indicator_alpha, ..p.text },
);
}
vec![frame.into_geometry()]
}
fn update(
&self,
_state: &mut (),
event: &canvas::Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<canvas::Action<M>> {
if self.data.suppressed { return None; }
let pos = cursor.position_in(bounds)?;
match event {
canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let frac = (pos.y / bounds.height).clamp(0.0, 1.0);
Some(canvas::Action::publish((self.on_jump)(frac)).and_capture())
}
_ => None,
}
}
fn mouse_interaction(
&self,
_state: &(),
bounds: Rectangle,
cursor: mouse::Cursor,
) -> mouse::Interaction {
if !self.data.suppressed && cursor.is_over(bounds) {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
}
}
/// builds a minimap canvas pinned at a fixed width.
pub fn minimap<'a, M, F>(
data: MinimapData,
width: f32,
on_jump: F,
) -> Element<'a, M, Theme, iced_wgpu::Renderer>
where
M: Clone + 'static,
F: Fn(f32) -> M + 'a,
{
canvas::Canvas::new(MinimapProgram::<M, F> {
data,
on_jump,
_marker: std::marker::PhantomData,
})
.width(Length::Fixed(width))
.height(Length::Fill)
.into()
}