tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 modules/mini-map/src/lib.rs Rust · 274 siq'ikuna · 9.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1//! `llimphi-module-mini-map` — minimap del buffer activo.
2//!
3//! Equivalente al "Minimap" de VS Code / "thumbnail" de Sublime: un
4//! panel angosto pegado al editor que pinta una linea horizontal por
5//! cada linea del buffer (ancho ~= len_chars, cap a `usable_w`),
6//! resalta el viewport visible como rect translucido y marca el caret.
7//! Click sobre el minimap salta esa linea al editor.
8//!
9//! El modulo es agnostico del editor: el host pasa un slice con la
10//! cantidad de chars por linea, el rango visible y la linea del
11//! caret. No depende de `llimphi-widget-text-editor` — cualquier
12//! buffer (rope, vec<String>, archivo memmaped) sirve.
13//!
14//! Sigue el contrato Llimphi de `docs/MODULES.md`:
15//! `State + Msg + Action + apply/on_key/open_shortcut/view + Palette`.
16
17#![forbid(unsafe_code)]
18
19use std::sync::Arc;
20
21use llimphi_compositor::llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
22use llimphi_compositor::peniko::kurbo::{Affine, Rect as KRect};
23use llimphi_compositor::peniko::{Color, Fill};
24use llimphi_compositor::{Key, KeyEvent, KeyState, View};
25
26/// Capabilities que aporta este modulo al host.
27pub const CAPABILITIES: &[&str] = &["editor.mini-map"];
28
29/// Ancho del panel en pixeles (estilo VS Code).
30pub const PANEL_W: f32 = 120.0;
31/// Altura maxima por linea del buffer dentro del minimap (cap).
32pub const LINE_PX: f32 = 2.0;
33/// Escala chars->pixels para el ancho de cada slab. ~75 chars caben
34/// completos en `PANEL_W - PAD * 2`; lo demas se trunca.
35pub const CHAR_PX: f32 = 1.4;
36/// Padding lateral del panel (los slabs no tocan los bordes).
37pub const PAD: f32 = 6.0;
38
39/// Estado interno. Hoy efectivamente vacio — la informacion del buffer
40/// la pasa el host en cada frame via [`view`] — pero existe como
41/// `Option<MiniMapState>` en el host para representar abierto/cerrado
42/// y para futuras extensiones (scrubbing, fold-aware, syntax per slab).
43#[derive(Debug, Default, Clone)]
44pub struct MiniMapState {
45    /// Reservado para drag-scrub: la y inicial en pixeles dentro del
46    /// panel cuando el usuario empieza a arrastrar. `None` = sin drag
47    /// activo. Hoy no se consume (click es suficiente); declarado para
48    /// que el contrato del state no cambie cuando se agregue.
49    pub drag_anchor_y: Option<f32>,
50}
51
52impl MiniMapState {
53    pub fn new() -> Self {
54        Self::default()
55    }
56}
57
58/// Vocabulario interno. El host lo wrapea en su Msg.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum MiniMapMsg {
61    /// Convencional: el host abre el panel guardando un `MiniMapState`
62    /// en el modelo. El modulo no construye state global.
63    Open,
64    Close,
65    /// El usuario clickeo o arrastro: salta a la linea indicada.
66    Jump(usize),
67}
68
69/// Efecto solicitado al host.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum MiniMapAction {
72    None,
73    /// El host deberia remover el state del modelo.
74    Close,
75    /// El host deberia centrar el viewport en esta linea del buffer
76    /// activo. El modulo NO se cierra — el minimap es persistente.
77    JumpTo(usize),
78}
79
80/// Snapshot del buffer que el host pasa en cada frame. El modulo no
81/// copia, solo lee. La cantidad de chars por linea es lo unico que
82/// necesita para dibujar; viewport + caret se overlayean encima.
83pub struct Snapshot<'a> {
84    /// `lines[i]` = numero de chars (no bytes) en la linea `i`.
85    pub lines: &'a [usize],
86    /// Rango visible en el editor: `[start, end)`.
87    pub viewport_start: usize,
88    pub viewport_end: usize,
89    /// Linea del caret (0-based). Se pinta como marker accent.
90    pub caret_line: usize,
91}
92
93/// Aplica un mensaje al estado.
94pub fn apply(state: &mut MiniMapState, msg: MiniMapMsg) -> MiniMapAction {
95    match msg {
96        MiniMapMsg::Open => MiniMapAction::None,
97        MiniMapMsg::Close => MiniMapAction::Close,
98        MiniMapMsg::Jump(line) => {
99            state.drag_anchor_y = None;
100            MiniMapAction::JumpTo(line)
101        }
102    }
103}
104
105/// Routing de teclas. El minimap NO captura teclas (es un viewer
106/// pasivo). Devolvemos `None`; el host sigue su routing normal.
107pub fn on_key(_state: &MiniMapState, _event: &KeyEvent) -> Option<MiniMapMsg> {
108    None
109}
110
111/// Atajo recomendado: **Ctrl+Shift+M** (mnemonic M = Minimap).
112pub fn open_shortcut(event: &KeyEvent) -> bool {
113    event.state == KeyState::Pressed
114        && event.modifiers.ctrl
115        && event.modifiers.shift
116        && matches!(&event.key, Key::Character(s) if s.eq_ignore_ascii_case("m"))
117}
118
119/// Convierte una posicion-y dentro del panel a indice de linea. La
120/// conversion es proporcional al total de lineas; clamping en ambos
121/// extremos.
122pub fn y_to_line(y: f32, panel_h: f32, total_lines: usize) -> usize {
123    if total_lines == 0 || panel_h <= 0.0 {
124        return 0;
125    }
126    let t = (y / panel_h).clamp(0.0, 1.0);
127    let line = (t * total_lines as f32) as usize;
128    line.min(total_lines.saturating_sub(1))
129}
130
131/// Paleta visual derivable del theme.
132#[derive(Debug, Clone)]
133pub struct MiniMapPalette {
134    /// Fondo del panel del minimap.
135    pub bg_panel: Color,
136    /// Color de los slabs (uno por linea de buffer).
137    pub fg_slab: Color,
138    /// Color del rect translucido que marca el viewport visible.
139    pub bg_viewport: Color,
140    /// Borde del rect del viewport.
141    pub border_viewport: Color,
142    /// Color del marker del caret.
143    pub fg_caret: Color,
144}
145
146impl MiniMapPalette {
147    pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
148        Self {
149            bg_panel: t.bg_panel_alt,
150            fg_slab: t.fg_muted,
151            bg_viewport: with_alpha(t.bg_selected, 0.35),
152            border_viewport: t.border_focus,
153            fg_caret: t.accent,
154        }
155    }
156}
157
158fn with_alpha(c: Color, alpha: f32) -> Color {
159    let rgba = c.to_rgba8();
160    let a = (alpha.clamp(0.0, 1.0) * 255.0) as u8;
161    Color::from_rgba8(rgba.r, rgba.g, rgba.b, a)
162}
163
164/// Render del panel. `to_host` mapea cada `MiniMapMsg` al `Msg` de la app.
165/// `snapshot` es la vista del buffer en este frame (sin copia).
166///
167/// Layout: columna fija de `PANEL_W` px que ocupa todo el alto del
168/// contenedor padre. El host la mete en el `Row` del editor
169/// (tipicamente al final, al estilo VS Code).
170pub fn view<HostMsg, F>(
171    _state: &MiniMapState,
172    snapshot: &Snapshot,
173    palette: &MiniMapPalette,
174    to_host: F,
175) -> View<HostMsg>
176where
177    HostMsg: Clone + 'static,
178    F: Fn(MiniMapMsg) -> HostMsg + Copy + Send + Sync + 'static,
179{
180    // Capturamos por valor porque el painter es Arc<dyn Fn>: 'static + Send + Sync.
181    let lines: Vec<usize> = snapshot.lines.to_vec();
182    let viewport_start = snapshot.viewport_start;
183    let viewport_end = snapshot.viewport_end;
184    let caret_line = snapshot.caret_line;
185    let pal = palette.clone();
186
187    let total_lines = lines.len();
188    let click_host = to_host;
189    let on_click: Arc<dyn Fn(f32, f32, f32, f32) -> Option<HostMsg> + Send + Sync> = Arc::new(move |_x: f32, y: f32, _w: f32, h: f32| {
190        let line = y_to_line(y, h, total_lines);
191        Some(click_host(MiniMapMsg::Jump(line)))
192    });
193
194    let mut view = View::new(Style {
195        size: Size { width: length(PANEL_W), height: percent(1.0_f32) },
196        flex_shrink: 0.0,
197        flex_direction: FlexDirection::Column,
198        ..Default::default()
199    })
200    .fill(pal.bg_panel)
201    .clip(true)
202    .paint_with(move |scene, _ts, rect| {
203        if rect.w <= 0.0 || rect.h <= 0.0 || lines.is_empty() {
204            return;
205        }
206        let n = lines.len() as f32;
207        let line_h = (rect.h / n).min(LINE_PX);
208        let usable_w = (rect.w - PAD * 2.0).max(1.0);
209
210        // 1) Viewport overlay debajo de los slabs.
211        if viewport_end > viewport_start {
212            let y0 = rect.y + (viewport_start as f32 / n) * rect.h;
213            let y1 = rect.y + (viewport_end as f32 / n) * rect.h;
214            let vp = KRect::new(
215                rect.x as f64,
216                y0 as f64,
217                (rect.x + rect.w) as f64,
218                y1.max(y0 + 2.0) as f64,
219            );
220            scene.fill(Fill::NonZero, Affine::IDENTITY, pal.bg_viewport, None, &vp);
221        }
222
223        // 2) Slabs: uno por linea de buffer.
224        for (i, &chars) in lines.iter().enumerate() {
225            if chars == 0 {
226                continue;
227            }
228            let w = (chars as f32 * CHAR_PX).min(usable_w);
229            let y = rect.y + (i as f32 / n) * rect.h;
230            let slab_h = line_h.max(1.0);
231            let r = KRect::new(
232                (rect.x + PAD) as f64,
233                y as f64,
234                (rect.x + PAD + w) as f64,
235                (y + slab_h) as f64,
236            );
237            scene.fill(Fill::NonZero, Affine::IDENTITY, pal.fg_slab, None, &r);
238        }
239
240        // 3) Borde del viewport encima de los slabs.
241        if viewport_end > viewport_start {
242            let y0 = rect.y + (viewport_start as f32 / n) * rect.h;
243            let y1 = (rect.y + (viewport_end as f32 / n) * rect.h).max(y0 + 2.0);
244            let top = KRect::new(
245                rect.x as f64,
246                y0 as f64,
247                (rect.x + rect.w) as f64,
248                (y0 + 1.0) as f64,
249            );
250            let bot = KRect::new(
251                rect.x as f64,
252                (y1 - 1.0) as f64,
253                (rect.x + rect.w) as f64,
254                y1 as f64,
255            );
256            scene.fill(Fill::NonZero, Affine::IDENTITY, pal.border_viewport, None, &top);
257            scene.fill(Fill::NonZero, Affine::IDENTITY, pal.border_viewport, None, &bot);
258        }
259
260        // 4) Marker del caret: barra horizontal accent.
261        if caret_line < lines.len() {
262            let y = rect.y + (caret_line as f32 / n) * rect.h;
263            let r = KRect::new(
264                rect.x as f64,
265                y as f64,
266                (rect.x + rect.w) as f64,
267                (y + 2.0) as f64,
268            );
269            scene.fill(Fill::NonZero, Affine::IDENTITY, pal.fg_caret, None, &r);
270        }
271    });
272    view.raro_mut().on_click_at = Some(on_click);
273    view
274}