longcipher_leptos_components/components/editor/
minimap.rs

1//! Minimap navigation
2//!
3//! Provides VS Code-style minimap navigation for the editor.
4
5use leptos::prelude::*;
6
7/// Output from minimap interaction.
8#[derive(Debug, Clone, Default)]
9pub struct MinimapOutput {
10    /// Line to scroll to (if user clicked)
11    pub scroll_to_line: Option<usize>,
12    /// Whether the minimap is being dragged
13    pub is_dragging: bool,
14}
15
16/// A VS Code-style minimap component.
17///
18/// Displays a zoomed-out view of the document for quick navigation.
19///
20/// # Example
21///
22/// ```rust,ignore
23/// <Minimap
24///     content=content
25///     scroll_line=scroll_line
26///     visible_lines=30
27///     width=100.0
28///     on_navigate=move |line| scroll_to_line(line)
29/// />
30/// ```
31#[component]
32pub fn Minimap(
33    /// Document content
34    #[prop(into)]
35    content: Signal<String>,
36
37    /// Current scroll line
38    #[prop(into)]
39    scroll_line: Signal<usize>,
40
41    /// Number of visible lines in viewport
42    #[prop(optional, default = 30)]
43    visible_lines: usize,
44
45    /// Width in pixels
46    #[prop(optional, default = 80.0)]
47    width: f32,
48
49    /// Show search highlights (reserved for future use)
50    #[prop(optional, default = false)]
51    #[allow(unused_variables)]
52    show_highlights: bool,
53
54    /// Navigation callback
55    #[prop(into, optional)]
56    on_navigate: Option<Callback<usize>>,
57
58    /// Additional CSS classes
59    #[prop(into, optional)]
60    class: Option<String>,
61) -> impl IntoView {
62    // Calculate line count
63    let line_count = Memo::new(move |_| {
64        let text = content.get();
65        if text.is_empty() {
66            1
67        } else {
68            text.chars().filter(|&c| c == '\n').count() + 1
69        }
70    });
71
72    // Handle click on minimap
73    let handle_click = move |ev: web_sys::MouseEvent| {
74        let target = event_target::<web_sys::HtmlElement>(&ev);
75        let rect = target.get_bounding_client_rect();
76        let y = ev.client_y() as f64 - rect.top();
77        let height = rect.height();
78
79        let total_lines = line_count.get();
80        let clicked_line = ((y / height) * total_lines as f64).floor() as usize;
81        let target_line = clicked_line.min(total_lines.saturating_sub(1));
82
83        if let Some(callback) = on_navigate.as_ref() {
84            callback.run(target_line);
85        }
86    };
87
88    // Calculate viewport indicator position and height
89    let viewport_style = move || {
90        let total = line_count.get();
91        let scroll = scroll_line.get();
92        let visible = visible_lines;
93
94        if total == 0 {
95            return "top: 0; height: 100%".to_string();
96        }
97
98        let top_percent = (scroll as f32 / total as f32) * 100.0;
99        let height_percent = (visible as f32 / total as f32).min(1.0) * 100.0;
100
101        format!(
102            "top: {:.1}%; height: {:.1}%",
103            top_percent.min(100.0 - height_percent),
104            height_percent
105        )
106    };
107
108    let css_class = move || {
109        let mut classes = vec!["leptos-minimap"];
110        if let Some(ref custom) = class {
111            classes.push(custom);
112        }
113        classes.join(" ")
114    };
115
116    view! {
117      <div class=css_class style=format!("width: {}px", width) on:click=handle_click>
118        // Document preview (simplified lines)
119        <div class="leptos-minimap-content">
120          {move || {
121            let text = content.get();
122            text
123              .lines()
124              .enumerate()
125              .map(|(i, line)| {
126                let line_width = (line.len() as f32 * 0.8).min(width - 8.0);
127                view! {
128                  <div
129                    class="leptos-minimap-line"
130                    style=format!("width: {}px", line_width)
131                    data-line=i
132                  />
133                }
134              })
135              .collect::<Vec<_>>()
136          }}
137        </div>
138
139        // Viewport indicator
140        <div class="leptos-minimap-viewport" style=viewport_style />
141      </div>
142    }
143}
144
145/// Default CSS styles for the minimap.
146pub const MINIMAP_STYLES: &str = r"
147.leptos-minimap {
148    position: relative;
149    background: rgba(0, 0, 0, 0.2);
150    border-left: 1px solid var(--editor-border, #3c3c3c);
151    cursor: pointer;
152    user-select: none;
153    overflow: hidden;
154}
155
156.leptos-minimap-content {
157    padding: 4px;
158}
159
160.leptos-minimap-line {
161    height: 2px;
162    margin-bottom: 1px;
163    background: var(--editor-fg, #d4d4d4);
164    opacity: 0.3;
165    border-radius: 1px;
166}
167
168.leptos-minimap-viewport {
169    position: absolute;
170    left: 0;
171    right: 0;
172    background: rgba(255, 255, 255, 0.1);
173    border: 1px solid rgba(255, 255, 255, 0.2);
174    pointer-events: none;
175}
176
177.leptos-minimap:hover .leptos-minimap-viewport {
178    background: rgba(255, 255, 255, 0.15);
179}
180";