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}