Skip to main content

recovery_ui/
console.rs

1// Copyright 2022 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 anyhow::Error;
6use carnelian::color::Color;
7use carnelian::drawing::{linebreak_text, FontFace};
8use carnelian::render::Context as RenderContext;
9use carnelian::scene::facets::{TextFacetOptions, TextHorizontalAlignment, TextVerticalAlignment};
10use carnelian::scene::layout::{
11    CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize,
12};
13use carnelian::scene::scene::{Scene, SceneBuilder};
14use carnelian::{Message, Point, Size, ViewAssistant, ViewAssistantContext};
15use zx::Event;
16
17const TEXT_FONT_SIZE: f32 = 24.0;
18const MAX_CONSOLE_LINE_COUNT: usize = 20;
19const CONSOLE_WELCOME_MESSAGE: &str = "Welcome. Tap top-left to exit debug console.";
20
21#[derive(Debug)]
22pub enum ConsoleMessages {
23    // Appends provided text to the Console output.
24    AddText(String),
25}
26
27// Caches the composited Scene built in console_scene().
28pub struct SceneDetails {
29    pub(crate) scene: Scene,
30}
31
32pub struct ConsoleViewAssistant {
33    font_face: FontFace,
34    // SceneDetails created and cached by console_scene().
35    scene_details: Option<SceneDetails>,
36    // Console output lines.
37    lines: Vec<String>,
38    // Width of the area where the view will be rendered.
39    layout_width: Option<f32>,
40}
41
42impl ConsoleViewAssistant {
43    pub fn new(font_face: FontFace) -> Result<ConsoleViewAssistant, Error> {
44        // Fill with blanks to "maximize" the area consumed for display.
45        let mut lines: Vec<String> = Vec::new();
46        for _ in 1..MAX_CONSOLE_LINE_COUNT {
47            lines.push("".to_string());
48        }
49        lines.push(CONSOLE_WELCOME_MESSAGE.to_string());
50
51        Ok(ConsoleViewAssistant { font_face, scene_details: None, lines, layout_width: None })
52    }
53
54    // Returns cached SceneDetails if available, otherwise builds one from scratch and caches result.
55    pub fn console_scene(&mut self, context: &ViewAssistantContext) -> SceneDetails {
56        let scene_details = self.scene_details.take().unwrap_or_else(|| {
57            let target_size = context.size;
58            self.layout_width = Some(target_size.width.into());
59
60            let mut builder = SceneBuilder::new().background_color(Color::new());
61            builder
62                .group()
63                .column()
64                .max_size()
65                .main_align(MainAxisAlignment::SpaceEvenly)
66                .contents(|builder| {
67                    builder.start_group(
68                        "text_row",
69                        Flex::with_options_ptr(FlexOptions::row(
70                            MainAxisSize::Max,
71                            MainAxisAlignment::Start,
72                            CrossAxisAlignment::End,
73                        )),
74                    );
75                    builder.text(
76                        self.font_face.clone(),
77                        &self.lines.join("\n"),
78                        TEXT_FONT_SIZE,
79                        Point::zero(),
80                        TextFacetOptions {
81                            horizontal_alignment: TextHorizontalAlignment::Left,
82                            vertical_alignment: TextVerticalAlignment::Top,
83                            color: Color::green(),
84                            ..TextFacetOptions::default()
85                        },
86                    );
87                    builder.end_group();
88                });
89
90            // Create a scene from the builder constructed above.
91            let mut scene = builder.build();
92
93            scene.layout(target_size);
94            SceneDetails { scene }
95        });
96
97        scene_details
98    }
99}
100
101impl ViewAssistant for ConsoleViewAssistant {
102    fn resize(&mut self, _new_size: &Size) -> Result<(), Error> {
103        self.scene_details = None;
104
105        Ok(())
106    }
107
108    // Called repeatedly from ProxyViewAssistant's render() when Console is active.
109    fn render(
110        &mut self,
111        render_context: &mut RenderContext,
112        ready_event: Event,
113        context: &ViewAssistantContext,
114    ) -> Result<(), Error> {
115        let mut scene_details = self.console_scene(context);
116        scene_details.scene.render(render_context, ready_event, context)?;
117        self.scene_details = Some(scene_details);
118
119        context.request_render();
120
121        Ok(())
122    }
123
124    fn setup(&mut self, context: &ViewAssistantContext) -> Result<(), Error> {
125        // Store width so we know where to wrap text.
126        let target_size = context.size;
127        self.layout_width = Some(target_size.width.into());
128
129        Ok(())
130    }
131
132    // Adds text provided by ConsoleMessage::AddText message as a Console line.
133    fn handle_message(&mut self, message: Message) {
134        if let Some(console_message) = message.downcast_ref::<ConsoleMessages>() {
135            match console_message {
136                ConsoleMessages::AddText(text) => {
137                    for line in text.split("\n") {
138                        self.lines.push(line.to_string());
139                    }
140
141                    if let Some(max_width) = self.layout_width {
142                        let mut wrapped_lines = Vec::new();
143
144                        for line in &self.lines {
145                            wrapped_lines.extend(linebreak_text(
146                                &self.font_face,
147                                TEXT_FONT_SIZE,
148                                &line,
149                                max_width,
150                            ));
151                        }
152
153                        self.lines = wrapped_lines;
154                    }
155
156                    if self.lines.len() > MAX_CONSOLE_LINE_COUNT {
157                        self.lines.drain(0..self.lines.len() - MAX_CONSOLE_LINE_COUNT);
158                    }
159
160                    // Force scene to be rebuilt with new lines on next render().
161                    self.scene_details = None;
162                }
163            }
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use carnelian::make_message;
172
173    const TEST_MESSAGE: &str = "Test message";
174    const SMALL_MULTILINE_TEST_MESSAGE: &str = "1\n2\n3\n4\n5";
175    const GIANT_MULTILINE_TEST_MESSAGE: &str =
176        "1\n2\n3\n4\n5\n6\n7\n8\n9\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT";
177    const LONG_TEST_MESSAGE: &str =
178        "this is a very long message that will get split into multiple lines because the screen is \
179        not wide enough to hold it";
180
181    // This font face initialisation is copied from the Carnelian test in facets.rs
182    static FONT_DATA: &'static [u8] = include_bytes!(
183        "../../../../../prebuilt/third_party/fonts/robotoslab/RobotoSlab-Regular.ttf"
184    );
185    static FONT_FACE: std::sync::LazyLock<FontFace> =
186        std::sync::LazyLock::new(|| FontFace::new(&FONT_DATA).expect("Failed to create font"));
187
188    #[test]
189    fn test_add_text_message_modifies_lines() -> std::result::Result<(), anyhow::Error> {
190        let font_face = FONT_FACE.clone();
191        let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
192
193        // Verify line buffer is "initialized full" after console_view_assistant is constructed.
194        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
195
196        // Add text to Console by requesting it handle an "AddText" message.
197        console_view_assistant
198            .handle_message(make_message(ConsoleMessages::AddText(TEST_MESSAGE.to_string())));
199
200        // Verify old data has been culled.
201        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
202
203        // Verify our newest Console message contains expected value.
204        assert_eq!(
205            console_view_assistant.lines.last().unwrap().to_string(),
206            TEST_MESSAGE.to_string()
207        );
208
209        Ok(())
210    }
211
212    #[test]
213    fn test_small_multiline_messages_get_split() -> std::result::Result<(), anyhow::Error> {
214        let font_face = FONT_FACE.clone();
215        let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
216
217        // Add a multiline message to Console.
218        console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(
219            SMALL_MULTILINE_TEST_MESSAGE.to_string(),
220        )));
221
222        // Verify the tail of our Console messages are the individual lines.
223        let expected_lines = SMALL_MULTILINE_TEST_MESSAGE.split("\n").collect::<Vec<&str>>();
224        for (i, expected) in expected_lines.iter().enumerate() {
225            let actual = &console_view_assistant.lines
226                [console_view_assistant.lines.len() - expected_lines.len() + i];
227            assert_eq!(expected, actual, "Multiline message not split as expected");
228        }
229
230        // Verify our overall line count still "fills the screen."
231        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
232
233        Ok(())
234    }
235
236    #[test]
237    fn test_giant_multiline_messages_get_split() -> std::result::Result<(), anyhow::Error> {
238        let font_face = FONT_FACE.clone();
239        let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
240
241        // Add a giant (more rows than the screen can display) multiline message to Console.
242        console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(
243            GIANT_MULTILINE_TEST_MESSAGE.to_string(),
244        )));
245
246        // Verify the lines in our Console are just the "underflow" lines from the giant message.
247        let expected_lines = &GIANT_MULTILINE_TEST_MESSAGE.rsplit("\n").collect::<Vec<&str>>()
248            [..MAX_CONSOLE_LINE_COUNT];
249        for (i, expected) in expected_lines.iter().rev().enumerate() {
250            let actual = &console_view_assistant.lines
251                [console_view_assistant.lines.len() - expected_lines.len() + i];
252            assert_eq!(expected, actual, "Multiline message not split as expected");
253        }
254
255        // Verify our overall line count still "fills the screen."
256        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
257
258        Ok(())
259    }
260
261    #[test]
262    fn test_long_messages_get_split() -> std::result::Result<(), anyhow::Error> {
263        let font_face = FONT_FACE.clone();
264        let max_width = 100f32;
265        let expected_lines =
266            linebreak_text(&font_face, TEXT_FONT_SIZE, LONG_TEST_MESSAGE, max_width);
267        assert!(expected_lines.len() > 1);
268
269        let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
270        console_view_assistant.layout_width = Some(max_width);
271        console_view_assistant
272            .handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
273
274        // Verify the lines in our Console are split into multiple lines.
275        for (i, expected) in expected_lines.iter().enumerate() {
276            let actual = &console_view_assistant.lines
277                [console_view_assistant.lines.len() - expected_lines.len() + i];
278            assert_eq!(expected, actual, "Message not split as expected");
279        }
280
281        // Verify our overall line count still "fills the screen."
282        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
283
284        Ok(())
285    }
286
287    #[test]
288    fn test_long_messages_get_split_after_multiple_messages(
289    ) -> std::result::Result<(), anyhow::Error> {
290        let font_face = FONT_FACE.clone();
291        let max_width = 300f32;
292        let mut expected_lines =
293            linebreak_text(&font_face, TEXT_FONT_SIZE, LONG_TEST_MESSAGE, max_width);
294
295        // Need two sets of lines for the two messages.
296        expected_lines.extend(expected_lines.clone());
297        assert!(expected_lines.len() > 1);
298
299        let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
300        console_view_assistant.layout_width = Some(max_width);
301        console_view_assistant
302            .handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
303
304        // A second message should still be split like the first.
305        console_view_assistant
306            .handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
307
308        // Verify the lines in our Console are split into multiple lines.
309        for (i, expected) in expected_lines.iter().enumerate() {
310            let actual = &console_view_assistant.lines
311                [console_view_assistant.lines.len() - expected_lines.len() + i];
312            assert_eq!(expected, actual, "Message not split as expected");
313        }
314
315        // Verify our overall line count still "fills the screen."
316        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
317
318        Ok(())
319    }
320
321    #[test]
322    fn test_long_paragraphs_get_split_preserving_paragraph_breaks(
323    ) -> std::result::Result<(), anyhow::Error> {
324        let font_face = FONT_FACE.clone();
325        let paragraphs = format!("{msg}\n{msg}\n{msg}", msg = LONG_TEST_MESSAGE);
326
327        let max_width = 300f32;
328        let lines = linebreak_text(&font_face, TEXT_FONT_SIZE, LONG_TEST_MESSAGE, max_width);
329        let mut expected_lines = Vec::new();
330
331        // Create three paragraphs to match `paragraphs`.
332        expected_lines.extend(lines.clone());
333        expected_lines.extend(lines.clone());
334        expected_lines.extend(lines);
335        assert!(expected_lines.len() > 1);
336
337        let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
338        console_view_assistant.layout_width = Some(max_width);
339        console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(paragraphs)));
340
341        // Verify the lines in our Console are split into multiple lines.
342        for (i, expected) in expected_lines.iter().enumerate() {
343            let actual = &console_view_assistant.lines
344                [console_view_assistant.lines.len() - expected_lines.len() + i];
345            assert_eq!(expected, actual, "Message not split as expected");
346        }
347
348        // Verify our overall line count still "fills the screen."
349        assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
350
351        Ok(())
352    }
353}