ailloli_ui_editor/paint/syntax_painter.rs
1//! Syntax token clipping, styling, and shaped text paint items.
2
3use ailloli_ui_core::TextStyle;
4use ailloli_ui_text::{StyledTextLayoutParams, StyledTextSpan, TextSystem, WrapMode};
5
6use crate::code::{CodeTheme, SyntaxKind, SyntaxToken};
7use crate::layout::EditorTextRun;
8use crate::paint::EditorPaintItem;
9use crate::EditorStyle;
10
11/// Builds one text paint item using syntax-styled spans when available.
12///
13/// Tokens are clipped and validated by [`styled_spans_for_run`]. When no
14/// effective span remains, the existing run layout is cloned. Otherwise a
15/// styled no-wrap layout is requested and the returned item retains the base
16/// foreground color for adapter fallback.
17///
18/// # Examples
19///
20/// ```
21/// use ailloli_ui_core::{Color, FontId, TextStyle};
22/// use ailloli_ui_editor::{code::{SyntaxKind, SyntaxToken}, layout::EditorTextRun, paint::syntax_painter::syntax_text_items_for_run, CodeTheme, EditorStyle};
23/// use ailloli_ui_text::{TextLayoutParams, TextSystem};
24/// let mut system = TextSystem::new();
25/// let layout = system.layout_cached(TextLayoutParams::new("fn", TextStyle::new(FontId::Mono, 13, Color::WHITE)));
26/// let run = EditorTextRun { index: 0, byte_range: 0..2, baseline_y: 12.0, layout };
27/// let items = syntax_text_items_for_run(0.0, 0.0, &run, &[SyntaxToken { range: 0..2, kind: SyntaxKind::Keyword }], EditorStyle::default(), CodeTheme::default(), &mut system);
28/// assert_eq!(items.len(), 1);
29/// ```
30pub fn syntax_text_items_for_run(
31 content_x: f32,
32 content_y: f32,
33 run: &EditorTextRun,
34 tokens: &[SyntaxToken],
35 style: EditorStyle,
36 theme: CodeTheme,
37 text_system: &mut TextSystem,
38) -> Vec<EditorPaintItem> {
39 let spans = styled_spans_for_run(run, tokens, style, theme);
40 if spans.is_empty() {
41 return vec![EditorPaintItem::Text {
42 pos: [content_x, content_y + run.baseline_y],
43 color: style.fg,
44 layout: run.layout.clone(),
45 }];
46 }
47 let base_style = TextStyle::new(style.font, style.px_size, style.fg);
48 let layout = text_system.layout_styled_cached(StyledTextLayoutParams {
49 text: run.layout.text(),
50 base_style,
51 spans: &spans,
52 max_width: None,
53 wrap_mode: WrapMode::NoWrap,
54 });
55 vec![EditorPaintItem::Text {
56 pos: [content_x, content_y + run.baseline_y],
57 color: style.fg,
58 layout,
59 }]
60}
61
62/// Converts source syntax tokens into validated run-local styled spans.
63///
64/// Tokens are clipped to the run, but ranges outside the run text or on invalid
65/// UTF-8 boundaries are skipped. Spans whose semantic color equals the base
66/// foreground are omitted. Input order and overlaps are preserved for the text
67/// system to resolve.
68///
69/// # Examples
70///
71/// ```
72/// use ailloli_ui_core::{Color, FontId, TextStyle};
73/// use ailloli_ui_editor::{code::{SyntaxKind, SyntaxToken}, layout::EditorTextRun, paint::syntax_painter::styled_spans_for_run, CodeTheme, EditorStyle};
74/// use ailloli_ui_text::{TextLayoutParams, TextSystem};
75/// let mut system = TextSystem::new();
76/// let layout = system.layout_cached(TextLayoutParams::new("fn x", TextStyle::new(FontId::Mono, 13, Color::WHITE)));
77/// let run = EditorTextRun { index: 0, byte_range: 10..14, baseline_y: 12.0, layout };
78/// let spans = styled_spans_for_run(&run, &[SyntaxToken { range: 10..12, kind: SyntaxKind::Keyword }], EditorStyle::default(), CodeTheme::default());
79/// assert_eq!(spans[0].range, 0..2);
80/// ```
81pub fn styled_spans_for_run(
82 run: &EditorTextRun,
83 tokens: &[SyntaxToken],
84 style: EditorStyle,
85 theme: CodeTheme,
86) -> Vec<StyledTextSpan> {
87 let text_len = run.layout.text().len();
88 let base_color = style.fg;
89 let mut spans = Vec::new();
90 for token in tokens {
91 let start = token.range.start.max(run.byte_range.start);
92 let end = token.range.end.min(run.byte_range.end);
93 if start >= end {
94 continue;
95 }
96 let local_start = start - run.byte_range.start;
97 let local_end = end - run.byte_range.start;
98 if local_start >= text_len
99 || local_end > text_len
100 || !run.layout.text().is_char_boundary(local_start)
101 || !run.layout.text().is_char_boundary(local_end)
102 {
103 continue;
104 }
105 let color = syntax_color(token.kind, theme);
106 if color == base_color {
107 continue;
108 }
109 spans.push(StyledTextSpan {
110 range: local_start..local_end,
111 style: TextStyle::new(style.font, style.px_size, color),
112 });
113 }
114 spans
115}
116
117/// Maps a syntax category to its theme color.
118fn syntax_color(kind: SyntaxKind, theme: CodeTheme) -> ailloli_ui_core::Color {
119 match kind {
120 SyntaxKind::Keyword => theme.syntax_keyword,
121 SyntaxKind::Type => theme.syntax_type,
122 SyntaxKind::Function => theme.syntax_function,
123 SyntaxKind::String => theme.syntax_string,
124 SyntaxKind::Number => theme.syntax_number,
125 SyntaxKind::Comment => theme.syntax_comment,
126 SyntaxKind::Operator => theme.syntax_operator,
127 SyntaxKind::Punctuation => theme.syntax_punctuation,
128 SyntaxKind::Identifier => theme.syntax_identifier,
129 }
130}