189 lines
5.3 KiB
Rust
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()
|
|
}
|