virtual_console_lib/
text_grid.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::colors::ColorScheme;
6use crate::terminal::TerminalConfig;
7use carnelian::color::Color;
8use carnelian::render::Context as RenderContext;
9use carnelian::scene::facets::Facet;
10use carnelian::scene::LayerGroup;
11use carnelian::{Size, ViewAssistantContext};
12use fuchsia_trace::duration;
13use std::any::Any;
14use std::cell::RefCell;
15use std::rc::Rc;
16use term_model::ansi::{CursorStyle, TermInfo};
17use term_model::term::cell::Flags;
18use term_model::term::color::Rgb;
19use term_model::Term;
20use terminal::{renderable_layers, FontSet, LayerContent, Offset, RenderableLayer, Renderer};
21
22fn make_rgb(color: &Color) -> Rgb {
23    Rgb { r: color.r, g: color.g, b: color.b }
24}
25
26/// Facet that implements a virtcon-style text grid with a status bar
27/// and terminal output.
28pub struct TextGridFacet<T> {
29    font_set: FontSet,
30    color_scheme: ColorScheme,
31    size: Size,
32    term: Option<Rc<RefCell<Term<T>>>>,
33    status: Vec<(String, Rgb)>,
34    status_tab_width: usize,
35    renderer: Renderer,
36}
37
38pub enum TextGridMessages<T> {
39    SetTermMessage(Rc<RefCell<Term<T>>>),
40    ChangeStatusMessage(Vec<(String, Rgb)>),
41}
42
43const STATUS_BG: Rgb = Rgb { r: 0, g: 0, b: 0 };
44
45impl<T> TextGridFacet<T> {
46    pub fn new(
47        font_set: FontSet,
48        cell_size: &Size,
49        color_scheme: ColorScheme,
50        term: Option<Rc<RefCell<Term<T>>>>,
51        status: Vec<(String, Rgb)>,
52        status_tab_width: usize,
53    ) -> Self {
54        let renderer = Renderer::new(&font_set, cell_size);
55
56        Self {
57            font_set,
58            color_scheme,
59            size: Size::zero(),
60            term,
61            status,
62            status_tab_width,
63            renderer,
64        }
65    }
66}
67
68impl<T: 'static> Facet for TextGridFacet<T> {
69    fn update_layers(
70        &mut self,
71        _: Size,
72        layer_group: &mut dyn LayerGroup,
73        render_context: &mut RenderContext,
74        view_context: &ViewAssistantContext,
75    ) -> std::result::Result<(), anyhow::Error> {
76        duration!(c"gfx", c"TextGrid::update_layers");
77
78        self.size = view_context.size;
79
80        let config: TerminalConfig = self.color_scheme.into();
81        let term = self.term.as_ref().map(|t| t.borrow());
82        let status_tab_width = self.status_tab_width;
83        let columns = term.as_ref().map(|t| t.cols().0).unwrap_or(1);
84        let bg = make_rgb(&self.color_scheme.back);
85        // First row is used for the status bar.
86        let term_offset = Offset { column: 0, row: 1 };
87
88        // Create an iterator over cells used for the status bar followed by active
89        // terminal cells. Each layer has an order, contents, and color.
90        //
91        // The order of layers will be stable unless the number of columns change.
92        //
93        // Status bar is a set of background layers, followed by foreground layers.
94        let layers = if STATUS_BG != bg {
95            Some((0..columns).into_iter().map(|x| RenderableLayer {
96                order: x,
97                column: x,
98                row: 0,
99                content: LayerContent::Cursor(CursorStyle::Block),
100                rgb: STATUS_BG,
101            }))
102        } else {
103            None
104        }
105        .into_iter()
106        .flat_map(|iter| iter)
107        .chain(self.status.iter().enumerate().flat_map(|(i, (s, rgb))| {
108            let start = i * status_tab_width;
109            let order = columns + start;
110            s.chars().enumerate().map(move |(x, c)| RenderableLayer {
111                order: order + x,
112                column: start + x,
113                row: 0,
114                content: LayerContent::Char((c, Flags::empty())),
115                rgb: *rgb,
116            })
117        }))
118        .chain(term.iter().flat_map(|term| renderable_layers(term, &config, &term_offset)));
119
120        self.renderer.render(layer_group, render_context, &self.font_set, layers);
121
122        Ok(())
123    }
124
125    fn handle_message(&mut self, message: Box<dyn Any>) {
126        if let Some(message) = message.downcast_ref::<TextGridMessages<T>>() {
127            match message {
128                TextGridMessages::SetTermMessage(term) => {
129                    self.term = Some(Rc::clone(term));
130                }
131                TextGridMessages::ChangeStatusMessage(status) => {
132                    self.status = status.clone();
133                }
134            }
135        }
136    }
137
138    fn calculate_size(&self, _: Size) -> Size {
139        self.size
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use anyhow::Error;
147    use carnelian::drawing::load_font;
148    use std::path::PathBuf;
149    use term_model::event::{Event, EventListener};
150
151    #[derive(Default)]
152    struct TestListener;
153
154    impl EventListener for TestListener {
155        fn send_event(&self, _event: Event) {}
156    }
157
158    const FONT: &'static str = "/pkg/data/font.ttf";
159
160    #[test]
161    fn can_create_text_grid() -> Result<(), Error> {
162        let font = load_font(PathBuf::from(FONT))?;
163        let font_set = FontSet::new(font, None, None, None, vec![]);
164        let _ = TextGridFacet::<TestListener>::new(
165            font_set,
166            &Size::new(8.0, 16.0),
167            ColorScheme::default(),
168            None,
169            vec![],
170            24,
171        );
172        Ok(())
173    }
174}