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}