use std::collections::{HashMap, HashSet};
use leptos::html;
use leptos::prelude::*;
use wasm_bindgen::JsCast;
use crate::web::visible_range;
use crate::web::code_editor::Highlighter;
fn fold_regions(lines: &[&str]) -> Vec<(usize, usize)> {
let mut stack: Vec<usize> = Vec::new();
let mut regions = Vec::new();
for (index, line) in lines.iter().enumerate() {
for character in line.chars() {
if character == '{' {
stack.push(index);
} else if character == '}'
&& let Some(start) = stack.pop()
&& index > start
{
regions.push((start, index));
}
}
}
regions
}
#[component]
pub fn CodeSurface(
#[prop(into)] value: Signal<String>,
#[prop(optional)] highlighter: Option<Highlighter>,
#[prop(default = 20.0)] line_height: f64,
#[prop(default = 420.0)] height: f64,
#[prop(default = 20)] overscan: usize,
) -> impl IntoView {
let folds = RwSignal::new(HashSet::<usize>::new());
let scroll_top = RwSignal::new(0.0);
let viewport_height = RwSignal::new(height);
let wrap_ref = NodeRef::<html::Div>::new();
let on_scroll = move |event: web_sys::Event| {
if let Some(element) = event
.target()
.and_then(|target| target.dyn_into::<web_sys::HtmlElement>().ok())
{
scroll_top.set(element.scroll_top() as f64);
viewport_height.set(element.client_height() as f64);
}
};
Effect::new(move |_| {
if let Some(element) = wrap_ref.get() {
viewport_height.set(element.client_height() as f64);
}
});
let model = Memo::new(move |_| {
let source = value.get();
let lines: Vec<String> = source.split('\n').map(str::to_string).collect();
let refs: Vec<&str> = lines.iter().map(String::as_str).collect();
let regions = fold_regions(&refs);
(lines, regions)
});
let body = move || {
let folded = folds.get();
let view_height = viewport_height.get().max(line_height);
let scroll = scroll_top.get();
model.with(|(lines, regions)| {
let mut hidden = HashSet::new();
for (start, end) in regions {
if folded.contains(start) {
for line in (start + 1)..=*end {
hidden.insert(line);
}
}
}
let headers: HashMap<usize, usize> = regions.iter().copied().collect();
let visible: Vec<usize> = (0..lines.len())
.filter(|line| !hidden.contains(line))
.collect();
let total = visible.len();
let (start, end) = visible_range(scroll, view_height, line_height, overscan, total);
let top_pad = start as f64 * line_height;
let bottom_pad = total.saturating_sub(end) as f64 * line_height;
let rows = visible[start..end]
.iter()
.map(|&line_index| {
let text = lines[line_index].clone();
let is_header = headers.contains_key(&line_index);
let is_folded = folded.contains(&line_index);
let spans = match highlighter {
Some(highlight) => highlight(&text),
None => vec![("tok-plain", text.clone())],
};
let toggle = move |_: web_sys::MouseEvent| {
folds.update(|set| {
if !set.remove(&line_index) {
set.insert(line_index);
}
});
};
view! {
<div class="nightshade-surface-row" style=format!("height:{line_height}px")>
<span class="nightshade-surface-num">{line_index + 1}</span>
{if is_header {
view! {
<span
class="nightshade-surface-fold"
class:folded=is_folded
on:click=toggle
>
"\u{25be}"
</span>
}
.into_any()
} else {
view! { <span class="nightshade-surface-fold-spacer"></span> }.into_any()
}}
<span class="nightshade-surface-text">
{spans
.into_iter()
.map(|(class, text)| view! { <span class=class>{text}</span> })
.collect_view()}
{is_folded.then(|| view! { <span class="nightshade-surface-ellipsis">"\u{2026}"</span> })}
</span>
</div>
}
})
.collect_view();
view! {
<div style=format!("height:{top_pad}px")></div>
{rows}
<div style=format!("height:{bottom_pad}px")></div>
}
})
};
view! {
<div
class="nightshade-code-surface-view"
node_ref=wrap_ref
style=format!("height:{height}px")
on:scroll=on_scroll
>
{body}
</div>
}
}