Skip to main content

sveltui_code/
code_preview.rs

1use crate::binding::CodeBinding;
2use crate::highlight::highlight_text;
3use crossterm::event::{KeyCode, KeyEvent};
4use crossterm::style::Color;
5use std::rc::Rc;
6use sveltui::element::scroll::{get_scroll, set_scroll};
7use sveltui::layout::Dimension;
8use sveltui::prelude::*;
9
10#[derive(Props)]
11pub struct CodePreviewProps {
12    #[prop(optional)]
13    pub id: Option<FocusId>,
14    #[prop(optional)]
15    pub value: Option<CodeBinding>,
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 b) = props.value {
58        b.get_text()
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                opt_title=props.title
191            >
192                {#each rendered_line_nodes as line_node}
193                    {line_node}
194                {/each}
195            </View>
196        </Pressable>
197    }
198}