terminal/
renderer.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::paths::{
6    maybe_path_for_char, maybe_path_for_cursor_style, path_for_strikeout, path_for_underline, Line,
7};
8use carnelian::color::Color;
9use carnelian::drawing::{FontFace, Glyph, TextGrid};
10use carnelian::render::{
11    BlendMode, Context as RenderContext, Fill, FillRule, Layer, Raster, Style,
12};
13use carnelian::scene::{LayerGroup, SceneOrder};
14use carnelian::Size;
15use euclid::{point2, Rect};
16use rustc_hash::{FxHashMap, FxHashSet};
17use std::collections::hash_map::Entry;
18use std::collections::BTreeSet;
19use std::mem;
20use term_model::ansi::{CursorStyle, TermInfo};
21use term_model::config::Config;
22use term_model::term::cell::Flags;
23use term_model::term::color::Rgb;
24use term_model::term::{RenderableCell, RenderableCellContent, Term};
25
26// Supported scale factors.
27//
28// These values are hard-coded in order to ensure that we use a grid size
29// that is efficient and aligns with physical pixels.
30const SCALE_FACTORS: &[f32] = &[1.0, 1.25, 2.0, 3.0, 4.0];
31
32/// Returns a scale factor given a set of DPI buckets and an actual DPI value.
33pub fn get_scale_factor(dpi: &BTreeSet<u32>, actual_dpi: f32) -> f32 {
34    let mut scale_factor = 0;
35    for value in dpi.iter() {
36        if *value as f32 > actual_dpi {
37            break;
38        }
39        scale_factor += 1;
40    }
41    *SCALE_FACTORS.get(scale_factor).unwrap_or(SCALE_FACTORS.last().unwrap())
42}
43
44/// Returns the cell size given a cell height.
45pub fn cell_size_from_cell_height(font_set: &FontSet, height: f32) -> Size {
46    let rounded_height = height.round();
47
48    // Use a cell width that matches the horizontal advance of character
49    // '0' as closely as possible. This minimizes the amount of horizontal
50    // stretching used for glyph outlines. Fallback to half of cell height
51    // if glyph '0' is missing.
52    let face = &font_set.font.face;
53    let width = face.glyph_index('0').map_or(height / 2.0, |glyph_index| {
54        let ascent = face.ascender() as f32;
55        let descent = face.descender() as f32;
56        let horizontal_advance =
57            face.glyph_hor_advance(glyph_index).expect("glyph_hor_advance") as f32;
58        rounded_height * horizontal_advance / (ascent - descent)
59    });
60
61    Size::new(width.round(), rounded_height)
62}
63
64#[derive(Clone)]
65pub struct FontSet {
66    font: FontFace,
67    bold_font: Option<FontFace>,
68    italic_font: Option<FontFace>,
69    bold_italic_font: Option<FontFace>,
70    fallback_fonts: Vec<FontFace>,
71}
72
73impl FontSet {
74    pub fn new(
75        font: FontFace,
76        bold_font: Option<FontFace>,
77        italic_font: Option<FontFace>,
78        bold_italic_font: Option<FontFace>,
79        fallback_fonts: Vec<FontFace>,
80    ) -> Self {
81        Self { font, bold_font, italic_font, bold_italic_font, fallback_fonts }
82    }
83}
84
85#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
86pub enum LayerContent {
87    Cursor(CursorStyle),
88    Char((char, Flags)),
89}
90
91// The term-model library gives us zero-width characters in our array of chars. However,
92// we do not support this at this point so we just pull out the first char for rendering.
93impl From<RenderableCell> for LayerContent {
94    fn from(cell: RenderableCell) -> Self {
95        match cell.inner {
96            RenderableCellContent::Cursor(cursor_key) => Self::Cursor(cursor_key.style),
97            RenderableCellContent::Chars(chars) => {
98                let flags = cell.flags & (Flags::BOLD_ITALIC | Flags::UNDERLINE | Flags::STRIKEOUT);
99                // Ignore hidden cells and render tabs as spaces to prevent font issues.
100                if chars[0] == '\t' || cell.flags.contains(Flags::HIDDEN) {
101                    Self::Char((' ', flags))
102                } else {
103                    Self::Char((chars[0], flags))
104                }
105            }
106        }
107    }
108}
109
110#[derive(PartialEq)]
111struct LayerId {
112    content: LayerContent,
113    rgb: Rgb,
114}
115
116fn maybe_raster_for_cursor_style(
117    render_context: &mut RenderContext,
118    cursor_style: CursorStyle,
119    cell_size: &Size,
120) -> Option<Raster> {
121    maybe_path_for_cursor_style(render_context, cursor_style, cell_size).as_ref().map(|p| {
122        let mut raster_builder = render_context.raster_builder().expect("raster_builder");
123        raster_builder.add(p, None);
124        raster_builder.build()
125    })
126}
127
128fn maybe_fallback_glyph_for_char(
129    render_context: &mut RenderContext,
130    c: char,
131    cell_size: &Size,
132) -> Option<Glyph> {
133    maybe_path_for_char(render_context, c, cell_size).as_ref().map(|p| {
134        let mut raster_builder = render_context.raster_builder().expect("raster_builder");
135        raster_builder.add(p, None);
136        let raster = raster_builder.build();
137        let bounding_box = Rect::from_size(*cell_size);
138        Glyph { raster, bounding_box }
139    })
140}
141
142fn maybe_glyph_for_char(
143    context: &mut RenderContext,
144    c: char,
145    flags: Flags,
146    textgrid: &TextGrid,
147    font_set: &FontSet,
148) -> Option<Glyph> {
149    let maybe_bold_italic_font = match flags & Flags::BOLD_ITALIC {
150        Flags::BOLD => font_set.bold_font.as_ref(),
151        Flags::ITALIC => font_set.italic_font.as_ref(),
152        Flags::BOLD_ITALIC => font_set.bold_italic_font.as_ref(),
153        _ => None,
154    };
155    let scale = textgrid.scale;
156    let offset = textgrid.offset;
157
158    // Glyph search order:
159    //
160    // 1. Bold/italic font first if appropriate.
161    // 2. Regular font.
162    // 3. Fallback fonts.
163    //
164    // The fallback font can be used to provide icons/emojis
165    // that are not expected to be part of the regular font.
166    for font in maybe_bold_italic_font
167        .iter()
168        .map(|font| *font)
169        .chain(std::iter::once(&font_set.font))
170        .chain(font_set.fallback_fonts.iter())
171    {
172        if let Some(glyph_index) = font.face.glyph_index(c) {
173            let glyph = Glyph::with_scale_and_offset(context, font, scale, offset, glyph_index);
174            return Some(glyph);
175        }
176    }
177
178    // Try fallback glyph if we failed to locate glyph in fonts.
179    maybe_fallback_glyph_for_char(context, c, &textgrid.cell_size)
180}
181
182fn maybe_raster_for_char(
183    context: &mut RenderContext,
184    c: char,
185    flags: Flags,
186    textgrid: &TextGrid,
187    font_set: &FontSet,
188) -> Option<Raster> {
189    // Get a potential glyph for this character.
190    let maybe_glyph = maybe_glyph_for_char(context, c, flags, textgrid, font_set);
191
192    // Create an extra raster if underline or strikeout flag is set.
193    let maybe_extra_raster = if flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT) {
194        let mut raster_builder = context.raster_builder().expect("raster_builder");
195        if flags.contains(Flags::UNDERLINE) {
196            // TODO(https://fxbug.dev/42172477): Avoid glyph overlap.
197            let line_metrics = font_set.font.face.underline_metrics();
198            raster_builder.add(
199                &path_for_underline(
200                    &textgrid.cell_size,
201                    context,
202                    line_metrics.map(|line_metrics| Line::new(line_metrics, textgrid)),
203                ),
204                None,
205            );
206        }
207        if flags.contains(Flags::STRIKEOUT) {
208            let line_metrics = font_set.font.face.strikeout_metrics();
209            raster_builder.add(
210                &path_for_strikeout(
211                    &textgrid.cell_size,
212                    context,
213                    line_metrics.map(|line_metrics| Line::new(line_metrics, textgrid)),
214                ),
215                None,
216            );
217        }
218        Some(raster_builder.build())
219    } else {
220        None
221    };
222
223    // Return a union of glyph raster and extra raster.
224    match (maybe_glyph, maybe_extra_raster) {
225        (Some(glyph), Some(extra_raster)) => Some(glyph.raster + extra_raster),
226        (Some(glyph), None) => Some(glyph.raster),
227        (None, Some(extra_raster)) => Some(extra_raster),
228        _ => None,
229    }
230}
231
232fn maybe_raster_for_layer_content(
233    render_context: &mut RenderContext,
234    content: &LayerContent,
235    column: usize,
236    row: usize,
237    textgrid: &TextGrid,
238    font_set: &FontSet,
239    raster_cache: &mut FxHashMap<LayerContent, Option<Raster>>,
240) -> Option<Raster> {
241    raster_cache
242        .entry(*content)
243        .or_insert_with(|| match content {
244            LayerContent::Cursor(cursor_style) => {
245                maybe_raster_for_cursor_style(render_context, *cursor_style, &textgrid.cell_size)
246            }
247            LayerContent::Char((c, flags)) => {
248                maybe_raster_for_char(render_context, *c, *flags, textgrid, font_set)
249            }
250        })
251        .as_ref()
252        .map(|r| {
253            let cell_size = &textgrid.cell_size;
254            let cell_position =
255                point2(cell_size.width * column as f32, cell_size.height * row as f32);
256            let raster = r.clone().translate(cell_position.to_vector().to_i32());
257            // Add empty raster to enable caching of the translated cursor.
258            // TODO: add more appropriate API for this.
259            let empty_raster = {
260                let raster_builder = render_context.raster_builder().unwrap();
261                raster_builder.build()
262            };
263            raster + empty_raster
264        })
265}
266
267fn make_color(term_color: &Rgb) -> Color {
268    Color { r: term_color.r, g: term_color.g, b: term_color.b, a: 0xff }
269}
270
271#[derive(PartialEq, Debug)]
272pub struct RenderableLayer {
273    pub order: usize,
274    pub column: usize,
275    pub row: usize,
276    pub content: LayerContent,
277    pub rgb: Rgb,
278}
279
280pub struct Offset {
281    pub column: usize,
282    pub row: usize,
283}
284
285pub fn renderable_layers<'b, T, C>(
286    term: &'b Term<T>,
287    config: &'b Config<C>,
288    offset: &'b Offset,
289) -> impl Iterator<Item = RenderableLayer> + 'b {
290    let columns = term.cols().0;
291    // renderable_cells() returns cells in painter's algorithm order, we
292    // convert that into a retained scene by assuming that we have at most
293    // 4 layers per cell:
294    //
295    // 1: Cursor background
296    // 2: Cursor foreground
297    // 3: Background
298    // 4: Foreground
299    let stride = columns * 4;
300    term.renderable_cells(config).flat_map(move |cell| {
301        let row = cell.line.0 + offset.row;
302        let cell_order = row * stride + (cell.column.0 + offset.column);
303        let content: LayerContent = cell.into();
304        let order = match content {
305            LayerContent::Cursor(_) => cell_order,
306            LayerContent::Char(_) => cell_order + columns * 2,
307        };
308        if cell.bg_alpha != 0.0 {
309            assert!(cell.bg_alpha == 1.0, "unsupported bg_alpha: {}", cell.bg_alpha);
310            Some(RenderableLayer {
311                order: order,
312                column: cell.column.0,
313                row,
314                content: LayerContent::Cursor(CursorStyle::Block),
315                rgb: cell.bg,
316            })
317        } else {
318            None
319        }
320        .into_iter()
321        .chain(std::iter::once(RenderableLayer {
322            order: order + columns,
323            column: cell.column.0,
324            row,
325            content,
326            rgb: cell.fg,
327        }))
328    })
329}
330
331pub struct Renderer {
332    textgrid: TextGrid,
333    raster_cache: FxHashMap<LayerContent, Option<Raster>>,
334    layers: FxHashMap<SceneOrder, LayerId>,
335    old_layers: FxHashSet<SceneOrder>,
336    new_layers: FxHashSet<SceneOrder>,
337}
338
339impl Renderer {
340    pub fn new(font_set: &FontSet, cell_size: &Size) -> Self {
341        let textgrid = TextGrid::new(&font_set.font, cell_size);
342        let raster_cache = FxHashMap::default();
343        let layers = FxHashMap::default();
344        let old_layers = FxHashSet::default();
345        let new_layers = FxHashSet::default();
346
347        Self { textgrid, raster_cache, layers, old_layers, new_layers }
348    }
349
350    pub fn render<I>(
351        &mut self,
352        layer_group: &mut dyn LayerGroup,
353        render_context: &mut RenderContext,
354        font_set: &FontSet,
355        layers: I,
356    ) where
357        I: IntoIterator<Item = RenderableLayer>,
358    {
359        let raster_cache = &mut self.raster_cache;
360        let textgrid = &self.textgrid;
361
362        // Process all layers and update the layer group as needed.
363        for RenderableLayer { order, column, row, content, rgb } in layers.into_iter() {
364            let id = LayerId { content, rgb };
365            let order = SceneOrder::try_from(order).unwrap_or_else(|e| panic!("{}", e));
366
367            // Remove from old layers.
368            self.old_layers.remove(&order);
369
370            match self.layers.entry(order) {
371                Entry::Occupied(entry) => {
372                    if *entry.get() != id {
373                        let raster = maybe_raster_for_layer_content(
374                            render_context,
375                            &id.content,
376                            column,
377                            row,
378                            textgrid,
379                            font_set,
380                            raster_cache,
381                        );
382                        if let Some(raster) = raster {
383                            let value = entry.into_mut();
384                            *value = id;
385
386                            let did_not_exist = self.new_layers.insert(order);
387                            assert!(
388                                did_not_exist,
389                                "multiple layers with order: {}",
390                                order.as_u32()
391                            );
392                            layer_group.insert(
393                                order,
394                                Layer {
395                                    raster,
396                                    clip: None,
397                                    style: Style {
398                                        fill_rule: FillRule::NonZero,
399                                        fill: Fill::Solid(make_color(&rgb)),
400                                        blend_mode: BlendMode::Over,
401                                    },
402                                },
403                            );
404                        } else {
405                            entry.remove_entry();
406                            layer_group.remove(order);
407                        }
408                    } else {
409                        let did_not_exist = self.new_layers.insert(order);
410                        assert!(did_not_exist, "multiple layers with order: {}", order.as_u32());
411                    }
412                }
413                Entry::Vacant(entry) => {
414                    let raster = maybe_raster_for_layer_content(
415                        render_context,
416                        &id.content,
417                        column,
418                        row,
419                        textgrid,
420                        font_set,
421                        raster_cache,
422                    );
423                    if let Some(raster) = raster {
424                        entry.insert(id);
425                        let did_not_exist = self.new_layers.insert(order);
426                        assert!(did_not_exist, "multiple layers with order: {}", order.as_u32());
427                        layer_group.insert(
428                            order,
429                            Layer {
430                                raster,
431                                clip: None,
432                                style: Style {
433                                    fill_rule: FillRule::NonZero,
434                                    fill: Fill::Solid(make_color(&rgb)),
435                                    blend_mode: BlendMode::Over,
436                                },
437                            },
438                        );
439                    }
440                }
441            }
442        }
443
444        // Remove any remaining old layers.
445        for order in self.old_layers.drain() {
446            self.layers.remove(&order);
447            layer_group.remove(order);
448        }
449
450        // Swap old layers for new layers.
451        mem::swap(&mut self.old_layers, &mut self.new_layers);
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use anyhow::Error;
459    use carnelian::drawing::DisplayRotation;
460    use carnelian::render::{generic, Context as RenderContext, ContextInner};
461    use euclid::size2;
462    use fuchsia_async as fasync;
463    use std::collections::BTreeMap;
464    use term_model::ansi::Processor;
465    use term_model::clipboard::Clipboard;
466    use term_model::event::{Event, EventListener};
467    use term_model::term::SizeInfo;
468
469    struct TermConfig;
470
471    impl Default for TermConfig {
472        fn default() -> TermConfig {
473            TermConfig
474        }
475    }
476
477    struct EventProxy;
478
479    impl EventListener for EventProxy {
480        fn send_event(&self, _event: Event) {}
481    }
482
483    // This font creation method isn't ideal. The correct method would be to ask the Fuchsia
484    // font service for the font data.
485    static FONT_DATA: &'static [u8] = include_bytes!(
486        "../../../../../prebuilt/third_party/fonts/robotomono/RobotoMono-Regular.ttf"
487    );
488    static FONT_SET: std::sync::LazyLock<FontSet> = std::sync::LazyLock::new(|| {
489        FontSet::new(
490            FontFace::new(&FONT_DATA).expect("Failed to create font"),
491            None,
492            None,
493            None,
494            vec![],
495        )
496    });
497
498    struct TestLayerGroup<'a>(&'a mut BTreeMap<SceneOrder, Layer>);
499
500    impl LayerGroup for TestLayerGroup<'_> {
501        fn clear(&mut self) {
502            self.0.clear();
503        }
504        fn insert(&mut self, order: SceneOrder, layer: Layer) {
505            self.0.insert(order, layer);
506        }
507        fn remove(&mut self, order: SceneOrder) {
508            self.0.remove(&order);
509        }
510    }
511
512    #[test]
513    fn check_scale_factors() {
514        let dpi = BTreeSet::from([160, 240, 320]);
515        assert_eq!(get_scale_factor(&dpi, 100.0), 1.0);
516        assert_eq!(get_scale_factor(&dpi, 180.0), 1.25);
517        assert_eq!(get_scale_factor(&dpi, 240.0), 2.0);
518        assert_eq!(get_scale_factor(&dpi, 319.0), 2.0);
519        assert_eq!(get_scale_factor(&dpi, 400.0), 3.0);
520    }
521
522    #[test]
523    fn can_create_renderable_layers() -> Result<(), Error> {
524        let cell_size = Size::new(8.0, 16.0);
525        let size_info = SizeInfo {
526            width: cell_size.width * 2.0,
527            height: cell_size.height,
528            cell_width: cell_size.width,
529            cell_height: cell_size.height,
530            padding_x: 0.0,
531            padding_y: 0.0,
532            dpr: 1.0,
533        };
534        let bg = Rgb { r: 0, g: 0, b: 0 };
535        let fg = Rgb { r: 255, g: 255, b: 255 };
536        let config = {
537            let mut config = Config::<TermConfig>::default();
538            config.colors.primary.background = bg;
539            config.colors.primary.foreground = fg;
540            config
541        };
542        let mut term = Term::new(&config, &size_info, Clipboard::new(), EventProxy {});
543        let mut parser = Processor::new();
544        let mut output = vec![];
545        parser.advance(&mut term, 'A' as u8, &mut output);
546        let offset = Offset { column: 0, row: 0 };
547        let result = renderable_layers(&term, &config, &offset).collect::<Vec<_>>();
548        assert_eq!(
549            result,
550            vec![
551                RenderableLayer {
552                    order: 6,
553                    column: 0,
554                    row: 0,
555                    content: LayerContent::Char(('A', Flags::empty())),
556                    rgb: fg
557                },
558                RenderableLayer {
559                    order: 3,
560                    column: 1,
561                    row: 0,
562                    content: LayerContent::Cursor(CursorStyle::Block),
563                    rgb: fg
564                },
565                RenderableLayer {
566                    order: 7,
567                    column: 1,
568                    row: 0,
569                    content: LayerContent::Char((' ', Flags::empty())),
570                    rgb: bg
571                }
572            ],
573            "unexpected layers"
574        );
575        Ok(())
576    }
577
578    #[fasync::run_singlethreaded(test)]
579    async fn can_render_cell() {
580        let size = size2(64, 64);
581        let forma_context = generic::Forma::new_context_without_token(size, DisplayRotation::Deg0);
582        let mut render_context = RenderContext { inner: ContextInner::Forma(forma_context) };
583        let mut renderer = Renderer::new(&FONT_SET, &Size::new(8.0, 16.0));
584        let layers = vec![
585            RenderableLayer {
586                order: 0,
587                column: 0,
588                row: 0,
589                content: LayerContent::Cursor(CursorStyle::Block),
590                rgb: Rgb { r: 0xff, g: 0xff, b: 0xff },
591            },
592            RenderableLayer {
593                order: 1,
594                column: 0,
595                row: 0,
596                content: LayerContent::Char(('A', Flags::empty())),
597                rgb: Rgb { r: 0, g: 0, b: 0xff },
598            },
599        ];
600        let mut result = BTreeMap::new();
601        let mut layer_group = TestLayerGroup(&mut result);
602        renderer.render(&mut layer_group, &mut render_context, &FONT_SET, layers.into_iter());
603        assert_eq!(result.len(), 2, "expected two layers");
604    }
605}