1use 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 AddText(String),
25}
26
27pub struct SceneDetails {
29 pub(crate) scene: Scene,
30}
31
32pub struct ConsoleViewAssistant {
33 font_face: FontFace,
34 scene_details: Option<SceneDetails>,
36 lines: Vec<String>,
38 layout_width: Option<f32>,
40}
41
42impl ConsoleViewAssistant {
43 pub fn new(font_face: FontFace) -> Result<ConsoleViewAssistant, Error> {
44 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 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 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 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 let target_size = context.size;
127 self.layout_width = Some(target_size.width.into());
128
129 Ok(())
130 }
131
132 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 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 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 assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
195
196 console_view_assistant
198 .handle_message(make_message(ConsoleMessages::AddText(TEST_MESSAGE.to_string())));
199
200 assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
202
203 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 console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(
219 SMALL_MULTILINE_TEST_MESSAGE.to_string(),
220 )));
221
222 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 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 console_view_assistant.handle_message(make_message(ConsoleMessages::AddText(
243 GIANT_MULTILINE_TEST_MESSAGE.to_string(),
244 )));
245
246 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 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 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 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 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 console_view_assistant
306 .handle_message(make_message(ConsoleMessages::AddText(LONG_TEST_MESSAGE.to_string())));
307
308 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 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 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 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 assert_eq!(console_view_assistant.lines.len(), MAX_CONSOLE_LINE_COUNT);
350
351 Ok(())
352 }
353}