longcipher_leptos_components/components/editor/
minimap.rs1use leptos::prelude::*;
6
7#[derive(Debug, Clone, Default)]
9pub struct MinimapOutput {
10 pub scroll_to_line: Option<usize>,
12 pub is_dragging: bool,
14}
15
16#[component]
32pub fn Minimap(
33 #[prop(into)]
35 content: Signal<String>,
36
37 #[prop(into)]
39 scroll_line: Signal<usize>,
40
41 #[prop(optional, default = 30)]
43 visible_lines: usize,
44
45 #[prop(optional, default = 80.0)]
47 width: f32,
48
49 #[prop(optional, default = false)]
51 #[allow(unused_variables)]
52 show_highlights: bool,
53
54 #[prop(into, optional)]
56 on_navigate: Option<Callback<usize>>,
57
58 #[prop(into, optional)]
60 class: Option<String>,
61) -> impl IntoView {
62 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 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 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 <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 <div class="leptos-minimap-viewport" style=viewport_style />
141 </div>
142 }
143}
144
145pub 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";