ailloli_ui_editor/paint/
code_decorations_painter.rs

1//! Search highlights and diagnostic underline paint construction.
2
3use ailloli_ui_core::Color;
4
5use crate::code::{CodeTheme, Diagnostic, DiagnosticSeverity, SearchState};
6use crate::input::selection::selection_rects_for_run;
7use crate::layout::EditorTextRun;
8use crate::paint::EditorPaintItem;
9use crate::EditorStyle;
10
11/// Builds background items for search matches intersecting one text run.
12///
13/// Search ranges are source-buffer byte offsets and are clipped to the run.
14/// Each visual-line segment becomes one item. The match whose slice index equals
15/// [`SearchState::active_index`] uses the active color and flag.
16///
17/// # Examples
18///
19/// ```
20/// use ailloli_ui_core::{Color, FontId, TextStyle};
21/// use ailloli_ui_editor::{layout::EditorTextRun, paint::code_decorations_painter::search_highlight_items_for_run, CodeTheme, DocumentVersion, EditorStyle, SearchQuery, SearchState};
22/// use ailloli_ui_text::{TextLayoutParams, TextSystem};
23/// let mut system = TextSystem::new();
24/// let layout = system.layout_cached(TextLayoutParams::new("abc", TextStyle::new(FontId::Mono, 13, Color::WHITE)));
25/// let run = EditorTextRun { index: 0, byte_range: 0..3, baseline_y: 12.0, layout };
26/// let mut search = SearchState::new(SearchQuery::new("b"));
27/// search.refresh("abc", DocumentVersion(1));
28/// let items = search_highlight_items_for_run(0.0, 0.0, &run, &search, EditorStyle::default(), CodeTheme::default());
29/// assert_eq!(items.len(), 1);
30/// ```
31pub fn search_highlight_items_for_run(
32    content_x: f32,
33    content_y: f32,
34    run: &EditorTextRun,
35    search: &SearchState,
36    style: EditorStyle,
37    theme: CodeTheme,
38) -> Vec<EditorPaintItem> {
39    search
40        .matches
41        .iter()
42        .enumerate()
43        .flat_map(|search_match| {
44            let (idx, search_match) = search_match;
45            let active = search.active_index == Some(idx);
46            clipped_local_range(run, search_match.range.clone()).map_or_else(Vec::new, |range| {
47                selection_rects_for_run(content_x, content_y, run, range.0, range.1, style)
48                    .into_iter()
49                    .map(|rect| EditorPaintItem::SearchHighlight {
50                        rect,
51                        color: if active {
52                            theme.search_active_match_bg
53                        } else {
54                            theme.search_match_bg
55                        },
56                        active,
57                    })
58                    .collect()
59            })
60        })
61        .collect()
62}
63
64/// Builds underline and optional active-fill items for run diagnostics.
65///
66/// Diagnostic ranges are half-open for painting and clipped to the run. Each
67/// selected visual segment gets a two-logical-pixel underline. An active
68/// diagnostic additionally emits a highlight before every underline; that
69/// highlight uses the first selected visual segment returned for the range.
70///
71/// # Examples
72///
73/// ```
74/// use ailloli_ui_core::{Color, FontId, TextStyle};
75/// use ailloli_ui_editor::{layout::EditorTextRun, paint::code_decorations_painter::diagnostic_underline_items_for_run, CodeTheme, Diagnostic, DiagnosticSeverity, EditorStyle};
76/// use ailloli_ui_text::{TextLayoutParams, TextSystem};
77/// let mut system = TextSystem::new();
78/// let layout = system.layout_cached(TextLayoutParams::new("abc", TextStyle::new(FontId::Mono, 13, Color::WHITE)));
79/// let run = EditorTextRun { index: 0, byte_range: 0..3, baseline_y: 12.0, layout };
80/// let diagnostics = [Diagnostic::new(1..2, DiagnosticSeverity::Error, "bad")];
81/// let items = diagnostic_underline_items_for_run(0.0, 0.0, &run, &diagnostics, None, EditorStyle::default(), CodeTheme::default());
82/// assert_eq!(items.len(), 1);
83/// ```
84pub fn diagnostic_underline_items_for_run(
85    content_x: f32,
86    content_y: f32,
87    run: &EditorTextRun,
88    diagnostics: &[Diagnostic],
89    active_index: Option<usize>,
90    style: EditorStyle,
91    theme: CodeTheme,
92) -> Vec<EditorPaintItem> {
93    diagnostics
94        .iter()
95        .enumerate()
96        .flat_map(|diagnostic| {
97            let (idx, diagnostic) = diagnostic;
98            let active = active_index == Some(idx);
99            clipped_local_range(run, diagnostic.range.clone()).map_or_else(Vec::new, |range| {
100                selection_rects_for_run(content_x, content_y, run, range.0, range.1, style)
101                    .into_iter()
102                    .flat_map(|mut rect| {
103                        rect.y = rect.y + rect.h - 2.0;
104                        rect.h = 2.0;
105                        let mut items = Vec::with_capacity(if active { 2 } else { 1 });
106                        if active {
107                            items.push(EditorPaintItem::SearchHighlight {
108                                rect: selection_rects_for_run(
109                                    content_x, content_y, run, range.0, range.1, style,
110                                )
111                                .into_iter()
112                                .next()
113                                .unwrap_or(rect),
114                                color: theme.diagnostic_active_bg,
115                                active: true,
116                            });
117                        }
118                        items.push(EditorPaintItem::DiagnosticUnderline {
119                            rect,
120                            color: diagnostic_color(theme, diagnostic.severity),
121                            active,
122                        });
123                        items
124                    })
125                    .collect()
126            })
127        })
128        .collect()
129}
130
131/// Clips a source-buffer range to run-local byte offsets.
132fn clipped_local_range(
133    run: &EditorTextRun,
134    range: std::ops::Range<usize>,
135) -> Option<(usize, usize)> {
136    if range.end <= run.byte_range.start || range.start >= run.byte_range.end {
137        return None;
138    }
139    let text_len = run.layout.text().len();
140    let lo = range
141        .start
142        .saturating_sub(run.byte_range.start)
143        .min(text_len);
144    let hi = range.end.saturating_sub(run.byte_range.start).min(text_len);
145    (hi > lo).then_some((lo, hi))
146}
147
148/// Maps diagnostic severity to its configured semantic color.
149///
150/// # Examples
151///
152/// ```
153/// use ailloli_ui_core::{Color, FontId, TextStyle};
154/// use ailloli_ui_editor::{layout::EditorTextRun, paint::{code_decorations_painter::diagnostic_underline_items_for_run, EditorPaintItem}, CodeTheme, Diagnostic, DiagnosticSeverity, EditorStyle};
155/// use ailloli_ui_text::{TextLayoutParams, TextSystem};
156/// let theme = CodeTheme::default();
157/// let mut system = TextSystem::new();
158/// let layout = system.layout_cached(TextLayoutParams::new("x", TextStyle::new(FontId::Mono, 13, Color::WHITE)));
159/// let run = EditorTextRun { index: 0, byte_range: 0..1, baseline_y: 12.0, layout };
160/// let items = diagnostic_underline_items_for_run(0.0, 0.0, &run, &[Diagnostic::new(0..1, DiagnosticSeverity::Warning, "warn")], None, EditorStyle::default(), theme);
161/// assert!(matches!(items[0], EditorPaintItem::DiagnosticUnderline { color, .. } if color == theme.diagnostic_warning));
162/// ```
163pub(crate) fn diagnostic_color(theme: CodeTheme, severity: DiagnosticSeverity) -> Color {
164    match severity {
165        DiagnosticSeverity::Error => theme.diagnostic_error,
166        DiagnosticSeverity::Warning => theme.diagnostic_warning,
167        DiagnosticSeverity::Info => theme.diagnostic_info,
168        DiagnosticSeverity::Hint => theme.diagnostic_hint,
169    }
170}