blob: ccf907fb550de393aff95bb5b2f3581420d67ec6 [file] [edit]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::Error;
use carnelian::color::Color;
use carnelian::drawing::{linebreak_text, FontFace};
use carnelian::render::Context as RenderContext;
use carnelian::scene::facets::{TextFacetOptions, TextHorizontalAlignment, TextVerticalAlignment};
use carnelian::scene::layout::{
CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize,
};
use carnelian::scene::scene::{Scene, SceneBuilder};
use carnelian::{Message, Point, Size, ViewAssistant, ViewAssistantContext};
use zx::Event;
const TEXT_FONT_SIZE: f32 = 24.0;
const MAX_CONSOLE_LINE_COUNT: usize = 20;
const CONSOLE_WELCOME_MESSAGE: &str = "Welcome. Tap top-left to exit debug console.";
#[derive(Debug)]
pub enum ConsoleMessages {
// Appends provided text to the Console output.
AddText(String),
}
// Caches the composited Scene built in console_scene().
pub struct SceneDetails {
pub(crate) scene: Scene,
}
pub struct ConsoleViewAssistant {
font_face: FontFace,
// SceneDetails created and cached by console_scene().
scene_details: Option<SceneDetails>,
// Console output lines.
lines: Vec<String>,
// Width of the area where the view will be rendered.
layout_width: Option<f32>,
}
impl ConsoleViewAssistant {
pub fn new(font_face: FontFace) -> Result<ConsoleViewAssistant, Error> {
// Fill with blanks to "maximize" the area consumed for display.
let mut lines: Vec<String> = Vec::new();
for _ in 1..MAX_CONSOLE_LINE_COUNT {
lines.push("".to_string());
}
lines.push(CONSOLE_WELCOME_MESSAGE.to_string());
Ok(ConsoleViewAssistant { font_face, scene_details: None, lines, layout_width: None })
}
// Returns cached SceneDetails if available, otherwise builds one from scratch and caches result.
pub fn console_scene(&mut self, context: &ViewAssistantContext) -> SceneDetails {
let scene_details = self.scene_details.take().unwrap_or_else(|| {
let target_size = context.size;
self.layout_width = Some(target_size.width.into());
let mut builder = SceneBuilder::new().background_color(Color::new());
builder
.group()
.column()
.max_size()
.main_align(MainAxisAlignment::SpaceEvenly)
.contents(|builder| {
builder.start_group(
"text_row",
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Max,
MainAxisAlignment::Start,
CrossAxisAlignment::End,
)),
);
builder.text(
self.font_face.clone(),
&self.lines.join("\n"),
TEXT_FONT_SIZE,
Point::zero(),
TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Left,
vertical_alignment: TextVerticalAlignment::Top,
color: Color::green(),
..TextFacetOptions::default()
},
);
builder.end_group();
});
// Create a scene from the builder constructed above.
let mut scene = builder.build();
scene.layout(target_size);
SceneDetails { scene }
});
scene_details
}
}
impl ViewAssistant for ConsoleViewAssistant {
fn resize(&mut self, _new_size: &Size) -> Result<(), Error> {
self.scene_details = None;
Ok(())
}
// Called repeatedly from ProxyViewAssistant's render() when Console is active.
fn render(
&mut self,
render_context: &mut RenderContext,
ready_event: Event,
context: &ViewAssistantContext,
) -> Result<(), Error> {
let mut scene_details = self.console_scene(context);
scene_details.scene.render(render_context, ready_event, context)?;
self.scene_details = Some(scene_details);
context.request_render();
Ok(())
}
fn setup(&mut self, context: &ViewAssistantContext) -> Result<(), Error> {
// Store width so we know where to wrap text.
let target_size = context.size;
self.layout_width = Some(target_size.width.into());
Ok(())
}
// Adds text provided by ConsoleMessage::AddText message as a Console line.
fn handle_message(&mut self, message: Message) {
if let Some(console_message) = message.downcast_ref::<ConsoleMessages>() {
match console_message {
ConsoleMessages::AddText(text) => {
for line in text.split("\n") {
self.lines.push(line.to_string());
}
if let Some(max_width) = self.layout_width {
let mut wrapped_lines = Vec::new();
for line in &self.lines {
wrapped_lines.extend(linebreak_text(
&self.font_face,
TEXT_FONT_SIZE,
&line,
max_width,
));
}
self.lines = wrapped_lines;
}
if self.lines.len() > MAX_CONSOLE_LINE_COUNT {
self.lines.drain(0..self.lines.len() - MAX_CONSOLE_LINE_COUNT);
}
// Force scene to be rebuilt with new lines on next render().
self.scene_details = None;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use carnelian::make_message;
const TEST_MESSAGE: &str = "Test message";
const SMALL_MULTILINE_TEST_MESSAGE: &str = "1\n2\n3\n4\n5";
const GIANT_MULTILINE_TEST_MESSAGE: &str =
"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";
const LONG_TEST_MESSAGE: &str =
"this is a very long message that will get split into multiple lines because the screen is \
not wide enough to hold it";
// This font face initialisation is copied from the Carnelian test in facets.rs
static FONT_DATA: &'static [u8] = include_bytes!(
"../../../../../prebuilt/third_party/fonts/robotoslab/RobotoSlab-Regular.ttf"
);
static FONT_FACE: std::sync::LazyLock<FontFace> =
std::sync::LazyLock::new(|| FontFace::new(&FONT_DATA).expect("Failed to create font"));
#[test]
fn test_add_text_message_modifies_lines() -> std::result::Result<(), anyhow::Error> {
let font_face = FONT_FACE.clone();
let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
// Verify line buffer is "initialized full" after console_view_assistant is constructed.
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
// Add text to Console by requesting it handle an "AddText" message.
console_view_assistant
.handle_message(make_message(ConsoleMessages::AddText(TEST_MESSAGE.to_string())));
// Verify old data has been culled.
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
// Verify our newest Console message contains expected value.
assert_eq!(
console_view_assistant.lines.last().unwrap().to_string(),
TEST_MESSAGE.to_string()
);
Ok(())
}
#[test]
fn test_small_multiline_messages_get_split() -> std::result::Result<(), anyhow::Error> {
let font_face = FONT_FACE.clone();
let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
// Add a multiline message to Console.
console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(
SMALL_MULTILINE_TEST_MESSAGE.to_string(),
)));
// Verify the tail of our Console messages are the individual lines.
let expected_lines = SMALL_MULTILINE_TEST_MESSAGE.split("\n").collect::<Vec<&str>>();
for (i, expected) in expected_lines.iter().enumerate() {
let actual = &console_view_assistant.lines
[console_view_assistant.lines.len() - expected_lines.len() + i];
assert_eq!(expected, actual, "Multiline message not split as expected");
}
// Verify our overall line count still "fills the screen."
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
Ok(())
}
#[test]
fn test_giant_multiline_messages_get_split() -> std::result::Result<(), anyhow::Error> {
let font_face = FONT_FACE.clone();
let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
// Add a giant (more rows than the screen can display) multiline message to Console.
console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(
GIANT_MULTILINE_TEST_MESSAGE.to_string(),
)));
// Verify the lines in our Console are just the "underflow" lines from the giant message.
let expected_lines = &GIANT_MULTILINE_TEST_MESSAGE.rsplit("\n").collect::<Vec<&str>>()
[..MAX_CONSOLE_LINE_COUNT];
for (i, expected) in expected_lines.iter().rev().enumerate() {
let actual = &console_view_assistant.lines
[console_view_assistant.lines.len() - expected_lines.len() + i];
assert_eq!(expected, actual, "Multiline message not split as expected");
}
// Verify our overall line count still "fills the screen."
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
Ok(())
}
#[test]
fn test_long_messages_get_split() -> std::result::Result<(), anyhow::Error> {
let font_face = FONT_FACE.clone();
let max_width = 100f32;
let expected_lines =
linebreak_text(&font_face, TEXT_FONT_SIZE, LONG_TEST_MESSAGE, max_width);
assert!(expected_lines.len() > 1);
let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
console_view_assistant.layout_width = Some(max_width);
console_view_assistant
.handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
// Verify the lines in our Console are split into multiple lines.
for (i, expected) in expected_lines.iter().enumerate() {
let actual = &console_view_assistant.lines
[console_view_assistant.lines.len() - expected_lines.len() + i];
assert_eq!(expected, actual, "Message not split as expected");
}
// Verify our overall line count still "fills the screen."
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
Ok(())
}
#[test]
fn test_long_messages_get_split_after_multiple_messages(
) -> std::result::Result<(), anyhow::Error> {
let font_face = FONT_FACE.clone();
let max_width = 300f32;
let mut expected_lines =
linebreak_text(&font_face, TEXT_FONT_SIZE, LONG_TEST_MESSAGE, max_width);
// Need two sets of lines for the two messages.
expected_lines.extend(expected_lines.clone());
assert!(expected_lines.len() > 1);
let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
console_view_assistant.layout_width = Some(max_width);
console_view_assistant
.handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
// A second message should still be split like the first.
console_view_assistant
.handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
// Verify the lines in our Console are split into multiple lines.
for (i, expected) in expected_lines.iter().enumerate() {
let actual = &console_view_assistant.lines
[console_view_assistant.lines.len() - expected_lines.len() + i];
assert_eq!(expected, actual, "Message not split as expected");
}
// Verify our overall line count still "fills the screen."
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
Ok(())
}
#[test]
fn test_long_paragraphs_get_split_preserving_paragraph_breaks(
) -> std::result::Result<(), anyhow::Error> {
let font_face = FONT_FACE.clone();
let paragraphs = format!("{msg}\n{msg}\n{msg}", msg = LONG_TEST_MESSAGE);
let max_width = 300f32;
let lines = linebreak_text(&font_face, TEXT_FONT_SIZE, LONG_TEST_MESSAGE, max_width);
let mut expected_lines = Vec::new();
// Create three paragraphs to match `paragraphs`.
expected_lines.extend(lines.clone());
expected_lines.extend(lines.clone());
expected_lines.extend(lines);
assert!(expected_lines.len() > 1);
let mut console_view_assistant = ConsoleViewAssistant::new(font_face).unwrap();
console_view_assistant.layout_width = Some(max_width);
console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(paragraphs)));
// Verify the lines in our Console are split into multiple lines.
for (i, expected) in expected_lines.iter().enumerate() {
let actual = &console_view_assistant.lines
[console_view_assistant.lines.len() - expected_lines.len() + i];
assert_eq!(expected, actual, "Message not split as expected");
}
// Verify our overall line count still "fills the screen."
assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
Ok(())
}
}