Skip to main content

re_ui/
syntax_highlighting.rs

1use egui::text::LayoutJob;
2use egui::{Color32, Style, TextFormat, TextStyle};
3use re_entity_db::InstancePath;
4use re_log_types::external::re_types_core::{
5    ArchetypeName, ComponentDescriptor, ComponentIdentifier, ComponentType,
6};
7use re_log_types::{ComponentPath, EntityPath, EntityPathPart, Instance};
8
9use crate::HasDesignTokens as _;
10
11// ----------------------------------------------------------------------------
12pub trait SyntaxHighlighting {
13    fn syntax_highlighted(&self, style: &Style) -> LayoutJob {
14        let mut builder = SyntaxHighlightedBuilder::new();
15        self.syntax_highlight_into(&mut builder);
16        builder.into_job(style)
17    }
18
19    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder);
20}
21
22// ----------------------------------------------------------------------------
23
24/// Easily build syntax-highlighted text.
25#[derive(Debug, Default)]
26pub struct SyntaxHighlightedBuilder {
27    text: String,
28    parts: smallvec::SmallVec<[SyntaxHighlightedPart; 1]>,
29}
30
31/// Easily build syntax-highlighted [`LayoutJob`]s.
32///
33/// Try to use one of the `append_*` or `with_*` methods that semantically matches
34/// what you are trying to highlight. Check the docs of the `append_*` methods for examples
35/// of what they should be used with.
36///
37/// The `with_*` methods are builder-style, taking `self` and returning `Self`.
38/// The `append_*` methods take `&mut self` and return `&mut Self`.
39///
40/// Use the `with_*` methods when building something inline.
41impl SyntaxHighlightedBuilder {
42    pub const QUOTE_CHAR: char = '"';
43
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Construct [`Self`] from an existing [`LayoutJob`].
49    ///
50    /// Some information (the `leading_space`) will be lost.
51    pub fn from(job: impl Into<LayoutJob>) -> Self {
52        let job = job.into();
53        Self {
54            text: job.text,
55            parts: job
56                .sections
57                .into_iter()
58                .map(|s| SyntaxHighlightedPart {
59                    style: SyntaxHighlightedStyle::Custom(Box::new(s.format)),
60                    byte_range: s.byte_range,
61                })
62                .collect(),
63        }
64    }
65
66    /// Append anything that implements [`SyntaxHighlighting`].
67    #[inline]
68    pub fn with(mut self, portion: &dyn SyntaxHighlighting) -> Self {
69        portion.syntax_highlight_into(&mut self);
70        self
71    }
72
73    /// Append anything that implements [`SyntaxHighlighting`].
74    #[inline]
75    pub fn append(&mut self, portion: &dyn SyntaxHighlighting) -> &mut Self {
76        portion.syntax_highlight_into(self);
77        self
78    }
79
80    fn append_kind(&mut self, style: SyntaxHighlightedStyle, portion: &str) -> &mut Self {
81        let start = self.text.len();
82        self.text.push_str(portion);
83        let end = self.text.len();
84        self.parts.push(SyntaxHighlightedPart {
85            byte_range: start..end,
86            style,
87        });
88        self
89    }
90}
91
92macro_rules! impl_style_fns {
93    ($docs:literal, $pure:ident, $with:ident, $append:ident, $style:ident) => {
94        impl_style_fns!($docs, $pure, $with, $append, (self, portion) {
95            self.append_kind(SyntaxHighlightedStyle::$style, portion);
96        });
97    };
98    ($docs:literal, $pure:ident, $with:ident, $append:ident, ($self:ident, $portion:ident) $content:expr) => {
99        #[doc = $docs]
100        #[inline]
101        pub fn $with(mut self, portion: &str) -> Self {
102            self.$append(portion);
103            self
104        }
105
106        #[doc = $docs]
107        #[inline]
108        pub fn $append(&mut $self, $portion: &str) -> &mut Self {
109            $content
110            $self
111        }
112
113        #[doc = $docs]
114        #[inline]
115        pub fn $pure(portion: &str) -> Self {
116            Self::new().$with(portion)
117        }
118    };
119}
120
121impl SyntaxHighlightedBuilder {
122    impl_style_fns!("null", null, with_null, append_null, Null);
123
124    impl_style_fns!(
125        "Some primitive value, e.g. a number or bool.",
126        primitive,
127        with_primitive,
128        append_primitive,
129        Primitive
130    );
131
132    impl_style_fns!(
133        "A string identifier.\n\nE.g. a variable name, field name, etc. Won't be quoted.",
134        identifier,
135        with_identifier,
136        append_identifier,
137        Identifier
138    );
139
140    impl_style_fns!(
141        "Some string data. Will be quoted.",
142        string_value,
143        with_string_value,
144        append_string_value,
145        (self, portion) {
146            let quote = Self::QUOTE_CHAR.to_string();
147            self.append_kind(SyntaxHighlightedStyle::StringValue, &quote);
148            self.append_kind(SyntaxHighlightedStyle::StringValue, portion);
149            self.append_kind(SyntaxHighlightedStyle::StringValue, &quote);
150        }
151    );
152
153    impl_style_fns!(
154        "A keyword, e.g. a filter operator, like `and` or `all`",
155        keyword,
156        with_keyword,
157        append_keyword,
158        Keyword
159    );
160
161    impl_style_fns!(
162        "An index number, e.g. an array index.",
163        index,
164        with_index,
165        append_index,
166        Index
167    );
168
169    impl_style_fns!(
170        "Some syntax, e.g. brackets, commas, colons, etc.",
171        syntax,
172        with_syntax,
173        append_syntax,
174        Syntax
175    );
176
177    impl_style_fns!(
178        "Body text, subdued (default label color).",
179        body,
180        with_body,
181        append_body,
182        Body
183    );
184
185    impl_style_fns!(
186        "Body text with default color (color of inactive buttons).",
187        body_default,
188        with_body_default,
189        append_body_default,
190        BodyDefault
191    );
192
193    impl_style_fns!(
194        "Body text in italics, e.g. for emphasis.",
195        body_italics,
196        with_body_italics,
197        append_body_italics,
198        BodyItalics
199    );
200
201    /// Append text with a custom format.
202    #[inline]
203    pub fn append_with_format(&mut self, text: &str, format: TextFormat) -> &mut Self {
204        self.append_kind(SyntaxHighlightedStyle::Custom(Box::new(format)), text);
205        self
206    }
207
208    /// Append text with a custom format closure.
209    #[inline]
210    pub fn append_with_format_closure<F>(&mut self, text: &str, f: F) -> &mut Self
211    where
212        F: 'static + Fn(&Style) -> TextFormat,
213    {
214        self.append_kind(SyntaxHighlightedStyle::CustomClosure(Box::new(f)), text);
215        self
216    }
217
218    /// With a custom format.
219    #[inline]
220    pub fn with_format(mut self, text: &str, format: TextFormat) -> Self {
221        self.append_with_format(text, format);
222        self
223    }
224
225    /// With a custom format closure.
226    #[inline]
227    pub fn with_format_closure<F>(mut self, text: &str, f: F) -> Self
228    where
229        F: 'static + Fn(&Style) -> TextFormat,
230    {
231        self.append_with_format_closure(text, f);
232        self
233    }
234}
235
236// ----------------------------------------------------------------------------
237
238impl SyntaxHighlightedBuilder {
239    #[inline]
240    pub fn into_job(self, style: &Style) -> LayoutJob {
241        let mut job = LayoutJob {
242            text: self.text,
243            sections: Vec::with_capacity(self.parts.len()),
244            ..Default::default()
245        };
246
247        for part in self.parts {
248            let format = part.style.into_format(style);
249            job.sections.push(egui::text::LayoutSection {
250                byte_range: part.byte_range,
251                format,
252                leading_space: 0.0,
253            });
254        }
255
256        job
257    }
258
259    #[inline]
260    pub fn into_widget_text(self, style: &Style) -> egui::WidgetText {
261        self.into_job(style).into()
262    }
263
264    pub fn text(&self) -> &str {
265        &self.text
266    }
267}
268
269// ----------------------------------------------------------------------------
270
271enum SyntaxHighlightedStyle {
272    StringValue,
273    Identifier,
274    Keyword,
275    Index,
276    Null,
277    Primitive,
278    Syntax,
279    Body,
280    BodyDefault,
281    BodyItalics,
282    Custom(Box<TextFormat>),
283    CustomClosure(Box<dyn Fn(&Style) -> TextFormat>),
284}
285
286impl std::fmt::Debug for SyntaxHighlightedStyle {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        match self {
289            Self::StringValue => write!(f, "StringValue"),
290            Self::Identifier => write!(f, "Identifier"),
291            Self::Keyword => write!(f, "Keyword"),
292            Self::Index => write!(f, "Index"),
293            Self::Null => write!(f, "Null"),
294            Self::Primitive => write!(f, "Primitive"),
295            Self::Syntax => write!(f, "Syntax"),
296            Self::Body => write!(f, "Body"),
297            Self::BodyDefault => write!(f, "BodyDefault"),
298            Self::BodyItalics => write!(f, "BodyItalics"),
299            Self::Custom(_) => write!(f, "Custom(…)"),
300            Self::CustomClosure(_) => write!(f, "CustomClosure(…)"),
301        }
302    }
303}
304
305#[derive(Debug)]
306struct SyntaxHighlightedPart {
307    byte_range: std::ops::Range<usize>,
308    style: SyntaxHighlightedStyle,
309}
310
311impl SyntaxHighlightedStyle {
312    /// Monospace text format with a specific color (that may be overridden by the style).
313    pub fn monospace_with_color(style: &Style, color: Color32) -> TextFormat {
314        TextFormat {
315            font_id: TextStyle::Monospace.resolve(style),
316            color: style.visuals.override_text_color.unwrap_or(color),
317            ..Default::default()
318        }
319    }
320
321    pub fn body_with_color(style: &Style, color: Color32) -> TextFormat {
322        TextFormat {
323            font_id: TextStyle::Body.resolve(style),
324            color: style.visuals.override_text_color.unwrap_or(color),
325            ..Default::default()
326        }
327    }
328
329    pub fn body(style: &Style) -> TextFormat {
330        Self::body_with_color(style, Color32::PLACEHOLDER)
331    }
332
333    pub fn into_format(self, style: &Style) -> TextFormat {
334        match self {
335            Self::StringValue => {
336                Self::monospace_with_color(style, style.tokens().code_string_color)
337            }
338            Self::Identifier => Self::monospace_with_color(style, style.tokens().text_default),
339            // TODO(lucas): Find a better way to deal with body / monospace style
340            Self::Keyword => Self::body_with_color(style, style.tokens().code_keyword_color),
341            Self::Index => Self::monospace_with_color(style, style.tokens().code_index_color),
342            Self::Null => Self::monospace_with_color(style, style.tokens().code_null_color),
343            Self::Primitive => {
344                Self::monospace_with_color(style, style.tokens().code_primitive_color)
345            }
346            Self::Syntax => Self::monospace_with_color(style, style.tokens().text_subdued),
347            Self::Body => Self::body(style),
348            Self::BodyDefault => {
349                let mut format = Self::body(style);
350                format.color = style
351                    .visuals
352                    .override_text_color
353                    .unwrap_or_else(|| style.tokens().text_default);
354                format
355            }
356            Self::BodyItalics => {
357                let mut format = Self::body(style);
358                format.italics = true;
359                format
360            }
361            Self::Custom(format) => *format,
362            Self::CustomClosure(f) => f(style),
363        }
364    }
365}
366
367// ----------------------------------------------------------------------------
368
369impl SyntaxHighlighting for EntityPathPart {
370    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
371        builder.append_identifier(&self.ui_string());
372    }
373}
374
375impl SyntaxHighlighting for Instance {
376    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
377        if self.is_all() {
378            builder.append_primitive("all");
379        } else {
380            builder.append_index(&re_format::format_uint(self.get()));
381        }
382    }
383}
384
385impl SyntaxHighlighting for EntityPath {
386    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
387        builder.append_syntax("/");
388
389        for (i, part) in self.iter().enumerate() {
390            if i != 0 {
391                builder.append_syntax("/");
392            }
393            builder.append(part);
394        }
395    }
396}
397
398impl SyntaxHighlighting for InstancePath {
399    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
400        builder.append(&self.entity_path);
401        if self.instance.is_specific() {
402            builder.append(&InstanceInBrackets(self.instance));
403        }
404    }
405}
406
407impl SyntaxHighlighting for ComponentType {
408    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
409        builder.append_identifier(self.short_name());
410    }
411}
412
413impl SyntaxHighlighting for ArchetypeName {
414    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
415        builder.append_identifier(self.short_name());
416    }
417}
418
419impl SyntaxHighlighting for ComponentIdentifier {
420    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
421        builder.append_identifier(self.as_ref());
422    }
423}
424
425impl SyntaxHighlighting for ComponentDescriptor {
426    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
427        builder.append_identifier(self.display_name());
428    }
429}
430
431impl SyntaxHighlighting for ComponentPath {
432    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
433        let Self {
434            entity_path,
435            component,
436        } = self;
437        builder
438            .append(entity_path)
439            .append_syntax(":")
440            .append(component);
441    }
442}
443
444/// Formats an instance number enclosed in square brackets: `[123]`
445pub struct InstanceInBrackets(pub Instance);
446
447impl SyntaxHighlighting for InstanceInBrackets {
448    fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
449        builder
450            .append_syntax("[")
451            .append(&self.0)
452            .append_syntax("]");
453    }
454}
455
456macro_rules! impl_sh_primitive {
457    ($t:ty, $to_string:path) => {
458        impl SyntaxHighlighting for $t {
459            fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
460                builder.append_primitive(&$to_string(*self));
461            }
462        }
463    };
464    ($t:ty) => {
465        impl SyntaxHighlighting for $t {
466            fn syntax_highlight_into(&self, builder: &mut SyntaxHighlightedBuilder) {
467                builder.append_primitive(&self.to_string());
468            }
469        }
470    };
471}
472
473impl_sh_primitive!(f32, re_format::format_f32);
474impl_sh_primitive!(f64, re_format::format_f64);
475
476impl_sh_primitive!(i8, re_format::format_int);
477impl_sh_primitive!(i16, re_format::format_int);
478impl_sh_primitive!(i32, re_format::format_int);
479impl_sh_primitive!(i64, re_format::format_int);
480impl_sh_primitive!(isize, re_format::format_int);
481impl_sh_primitive!(u8, re_format::format_uint);
482impl_sh_primitive!(u16, re_format::format_uint);
483impl_sh_primitive!(u32, re_format::format_uint);
484impl_sh_primitive!(u64, re_format::format_uint);
485impl_sh_primitive!(usize, re_format::format_uint);
486
487impl_sh_primitive!(bool);
488
489impl<T: SyntaxHighlighting> From<T> for SyntaxHighlightedBuilder {
490    fn from(portion: T) -> Self {
491        let mut builder = Self::new();
492        portion.syntax_highlight_into(&mut builder);
493        builder
494    }
495}