sveltui_code/
code_preview.rs1use crate::highlight::highlight_text;
2use crossterm::event::{KeyCode, KeyEvent};
3use crossterm::style::Color;
4use std::rc::Rc;
5use sveltui::element::scroll::{get_scroll, set_scroll};
6use sveltui::layout::Dimension;
7use sveltui::prelude::*;
8
9#[derive(Props)]
10pub struct CodePreviewProps {
11 #[prop(optional)]
12 pub id: Option<FocusId>,
13 #[prop(optional)]
15 pub value: Option<String>,
16 #[prop(optional)]
17 pub code: Option<String>,
18 #[prop(optional)]
19 pub language: Option<String>,
20 #[prop(optional)]
21 pub theme: Option<String>,
22 #[prop(default = true)]
23 pub show_line_numbers: bool,
24 #[prop(default = Dimension::Auto)]
25 pub width: Dimension,
26 #[prop(default = Dimension::Auto)]
27 pub height: Dimension,
28 #[prop(default = BorderStyle::Single)]
29 pub border: BorderStyle,
30 #[prop(optional)]
31 pub border_color: Option<Color>,
32 #[prop(optional)]
33 pub title: Option<String>,
34 #[prop(default = 4)]
35 pub tab_size: usize,
36}
37
38#[component]
39pub fn CodePreview(props: CodePreviewProps) -> impl IntoNode {
40 let id = props.id.unwrap_or_default();
41 let focused = is_focused(id);
42
43 let border_style = if focused {
44 BorderStyle::Double
45 } else {
46 props.border
47 };
48
49 let border_color = if focused {
50 Color::White
51 } else {
52 props.border_color.unwrap_or(Color::DarkGrey)
53 };
54
55 let raw_text = if let Some(ref c) = props.code {
56 c.clone()
57 } else if let Some(ref value) = props.value {
58 value.clone()
59 } else {
60 String::new()
61 };
62
63 let all_line_spans =
64 highlight_text(&raw_text, props.language.as_deref(), props.theme.as_deref());
65
66 let total_lines = all_line_spans.len().max(1);
67 let digits = format!("{}", total_lines).len().max(2);
68 let show_line_numbers = props.show_line_numbers;
69
70 let rendered_line_nodes: Vec<VNode> = all_line_spans
71 .into_iter()
72 .enumerate()
73 .map(|(offset, spans)| {
74 let line_num = offset + 1;
75 let mut row = ViewProps::builder()
76 .flex_direction(FlexDirection::Row)
77 .align_items(AlignItems::Center)
78 .justify_content(JustifyContent::FlexStart);
79 if show_line_numbers {
80 row = row.child(TextSpan(
81 TextSpanProps::builder()
82 .foreground_color(Color::DarkGrey)
83 .content(format!("{:>width$} │ ", line_num, width = digits))
84 .build(),
85 ));
86 }
87 if spans.is_empty() {
88 row = row.child(TextSpan(
89 TextSpanProps::builder()
90 .foreground_color(Color::DarkGrey)
91 .content(" ")
92 .build(),
93 ));
94 } else {
95 for span in spans {
96 row = row.child(TextSpan(
97 TextSpanProps::builder()
98 .foreground_color(span.fg)
99 .bold(span.bold)
100 .italic(span.italic)
101 .underline(span.underline)
102 .content(span.text)
103 .build(),
104 ));
105 }
106 }
107 View(row.build()).into_node()
108 })
109 .collect();
110
111 let title_str = props.title.clone().unwrap_or_else(|| {
112 if let Some(ref lang) = props.language {
113 format!("Preview ({lang})")
114 } else {
115 String::from("Code Preview")
116 }
117 });
118
119 let on_key: KeyHandler = Rc::new(move |key: KeyEvent, _ctx: &mut EventContext| {
120 let (sx, sy) = get_scroll(id);
121 let page_step = 10;
122
123 match key.code {
124 KeyCode::Up => {
125 set_scroll(id, (sx, sy.saturating_sub(1)));
126 EventResult::Handled
127 }
128 KeyCode::Down => {
129 set_scroll(id, (sx, sy.saturating_add(1)));
130 EventResult::Handled
131 }
132 KeyCode::PageUp => {
133 set_scroll(id, (sx, sy.saturating_sub(page_step)));
134 EventResult::Handled
135 }
136 KeyCode::PageDown => {
137 set_scroll(id, (sx, sy.saturating_add(page_step)));
138 EventResult::Handled
139 }
140 KeyCode::Home => {
141 set_scroll(id, (0, 0));
142 EventResult::Handled
143 }
144 KeyCode::End => {
145 set_scroll(id, (sx, u16::MAX));
146 EventResult::Handled
147 }
148 KeyCode::Left => {
149 set_scroll(id, (sx.saturating_sub(2), sy));
150 EventResult::Handled
151 }
152 KeyCode::Right => {
153 set_scroll(id, (sx.saturating_add(2), sy));
154 EventResult::Handled
155 }
156 _ => EventResult::Ignored,
157 }
158 });
159
160 let on_scroll: Rc<dyn Fn(i16, i16)> = Rc::new(move |_dx: i16, dy: i16| {
161 let (sx, sy) = get_scroll(id);
162 let ny = if dy > 0 {
163 sy.saturating_add(dy as u16)
164 } else {
165 sy.saturating_sub((-dy) as u16)
166 };
167 set_scroll(id, (sx, ny));
168 });
169
170 view! {
171 <Pressable
172 focus_id=id
173 tab_stop=true
174 on_key=on_key
175 semantics=Semantics::new(Role::Generic, title_str)
176 >
177 <View
178 border=border_style
179 border_color=border_color
180 padding=Padding::symmetric(1, 0)
181 width=props.width
182 height=props.height
183 flex_direction=FlexDirection::Column
184 align_items=AlignItems::FlexStart
185 justify_content=JustifyContent::FlexStart
186 overflow_x=Overflow::Scroll
187 overflow_y=Overflow::Scroll
188 scroll_id=id
189 on_scroll=on_scroll
190 title=props.title
191 >
192 {#each rendered_line_nodes as line_node}
193 {line_node}
194 {/each}
195 </View>
196 </Pressable>
197 }
198}