Skip to main content

recovery_ui/
generic_view.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 crate::button::{Button, ButtonMessages, ButtonOptions, ButtonShape, SceneBuilderButtonExt};
6use crate::constants::constants::*;
7use crate::font::get_default_font_face;
8use anyhow::Error;
9use carnelian::drawing::FontFace;
10use carnelian::render::rive::load_rive;
11use carnelian::render::Context as RenderContext;
12use carnelian::scene::facets::{
13    RiveFacet, TextFacetOptions, TextHorizontalAlignment, TextVerticalAlignment,
14};
15use carnelian::scene::layout::{
16    Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, Stack,
17    StackOptions,
18};
19use carnelian::scene::scene::{Scene, SceneBuilder};
20use carnelian::{
21    input, make_message, AppSender, Coord, Message, MessageTarget, Point, Size, ViewAssistant,
22    ViewAssistantContext, ViewKey,
23};
24use euclid::{size2, Size2D, UnknownUnit};
25use recovery_util::ota::state_machine::Event;
26use rive_rs::File;
27
28#[derive(Debug, Clone)]
29pub struct ButtonInfo {
30    pub text: &'static str,
31    pub icon_name: Option<&'static str>,
32    pub border: bool,
33    pub reversed: bool,
34    pub event: Event,
35}
36
37impl ButtonInfo {
38    pub fn new(
39        text: &'static str,
40        icon_name: Option<&'static str>,
41        border: bool,
42        reversed: bool,
43        event: Event,
44    ) -> Self {
45        Self { text, icon_name, border, reversed, event }
46    }
47}
48
49type Buttons = Vec<Button>;
50type ButtonInfos = Vec<ButtonInfo>;
51
52pub struct SceneDetails {
53    pub(crate) scene: Scene,
54    buttons: Buttons,
55}
56
57pub struct GenericSplitViewAssistant {
58    app_sender: AppSender,
59    view_key: ViewKey,
60    split: ScreenSplit,
61    text1: Option<String>,
62    text2: Option<String>,
63    text3: Option<String>,
64    button_infos1: Option<ButtonInfos>,
65    button_infos2: Option<ButtonInfos>,
66    permission_button_infos: Option<ButtonInfos>,
67    icon_name: Option<&'static str>,
68    icon_size: Option<Size2D<f32, UnknownUnit>>,
69    scene_details: Option<SceneDetails>,
70    icon_file: Option<File>,
71    logo_file: Option<File>,
72    qr_file: Option<File>,
73}
74
75impl GenericSplitViewAssistant {
76    // TODO(b/259793406) Consolidate into a struct, rename and document arguments to this function
77    pub fn new(
78        app_sender: AppSender,
79        view_key: ViewKey,
80        split: ScreenSplit,
81        text1: Option<String>,
82        text2: Option<String>,
83        text3: Option<String>,
84        button_infos1: Option<ButtonInfos>,
85        button_infos2: Option<ButtonInfos>,
86        permission_button_infos: Option<ButtonInfos>,
87        icon_name: Option<&'static str>,
88        icon_size: Option<Size2D<f32, UnknownUnit>>,
89    ) -> Result<GenericSplitViewAssistant, Error> {
90        #[cfg(feature = "debug_logging")]
91        println!("====== New generic view");
92        let icon_file = load_rive(ICONS_PATH).ok();
93        let logo_file = load_rive(LOGO_PATH).ok();
94        let qr_file = load_rive(QR_PATH).ok();
95
96        Ok(GenericSplitViewAssistant {
97            app_sender: app_sender,
98            view_key,
99            split,
100            text1,
101            text2,
102            text3,
103            button_infos1,
104            button_infos2,
105            permission_button_infos,
106            icon_name,
107            icon_size,
108            scene_details: None,
109            icon_file,
110            logo_file,
111            qr_file,
112        })
113    }
114
115    fn check_if_a_button_in_array_has_been_pressed(
116        &self,
117        text: &String,
118        button_infos: &ButtonInfos,
119    ) -> bool {
120        for button_info in button_infos {
121            // Generate the event that this button press has caused
122            if button_info.text == text {
123                #[cfg(feature = "debug_logging")]
124                println!("====== Generating event: {:?}", button_info.event);
125                self.app_sender.queue_message(
126                    MessageTarget::View(self.view_key),
127                    make_message(button_info.event.clone()),
128                );
129                return true;
130            }
131        }
132        false
133    }
134
135    fn check_if_any_button_has_been_pressed(&self, text: &String) {
136        let mut found = false;
137        if let Some(button_infos) = self.button_infos1.as_ref() {
138            found = self.check_if_a_button_in_array_has_been_pressed(text, button_infos);
139        }
140        if !found {
141            if let Some(button_infos) = self.button_infos2.as_ref() {
142                found = self.check_if_a_button_in_array_has_been_pressed(text, button_infos);
143            }
144        }
145        if !found {
146            if let Some(button_infos) = self.permission_button_infos.as_ref() {
147                self.check_if_a_button_in_array_has_been_pressed(text, button_infos);
148            }
149        }
150    }
151
152    fn generic_split_scene(&mut self, context: &ViewAssistantContext) -> SceneDetails {
153        let target_size = context.size;
154        let min_dimension = target_size.width.min(target_size.height);
155        let _padding = (min_dimension / 20.0).ceil().max(8.0);
156        let mut builder = SceneBuilder::new().background_color(BACKGROUND_WHITE);
157        let mut buttons: Buttons = Vec::new();
158        let split = self.split.as_percent();
159        let face = get_default_font_face();
160        builder.group().column().max_size().main_align(MainAxisAlignment::SpaceEvenly).contents(
161            |builder| {
162                builder.start_group(
163                    "main screen",
164                    Flex::with_options_ptr(FlexOptions::row(
165                        MainAxisSize::Max,
166                        MainAxisAlignment::SpaceEvenly,
167                        CrossAxisAlignment::Start,
168                    )),
169                );
170                builder.start_group(
171                    "left column",
172                    Flex::with_options_ptr(FlexOptions::column(
173                        MainAxisSize::Min,
174                        MainAxisAlignment::SpaceEvenly,
175                        CrossAxisAlignment::Start,
176                    )),
177                );
178                builder.start_group(
179                    "left row for margin space",
180                    Flex::with_options_ptr(FlexOptions::row(
181                        MainAxisSize::Min,
182                        MainAxisAlignment::Start,
183                        CrossAxisAlignment::Start,
184                    )),
185                );
186                builder.space(size2(LEFT_MARGIN_SPACE, 10.0));
187                builder.start_group(
188                    "left column contents",
189                    Flex::with_options_ptr(FlexOptions::column(
190                        MainAxisSize::Min,
191                        MainAxisAlignment::SpaceEvenly,
192                        CrossAxisAlignment::Start,
193                    )),
194                );
195                let width1 = target_size.width * split;
196                let width2 = target_size.width - width1;
197                builder.space(size2(width1, TOP_SPACE));
198                if let Some(text) = self.text1.as_ref() {
199                    builder.text(
200                        face.clone(),
201                        text,
202                        TITLE_FONT_SIZE,
203                        Point::zero(),
204                        TextFacetOptions {
205                            horizontal_alignment: TextHorizontalAlignment::Left,
206                            vertical_alignment: TextVerticalAlignment::Top,
207                            color: TITLE_COLOR,
208                            ..TextFacetOptions::default()
209                        },
210                    );
211                }
212
213                self.add_privacy_switch(&mut buttons, &face, builder, width1);
214
215                if let Some(button_infos) = self.button_infos1.as_ref() {
216                    buttons.append(&mut self.add_buttons(
217                        builder,
218                        button_infos,
219                        LARGE_BUTTON_FONT_SIZE,
220                    ));
221                }
222                builder.space(size2(10.0, AFTER_MAIN_BUTTON_SPACE));
223                builder.start_group(
224                    "bottom button row",
225                    Flex::with_options_ptr(FlexOptions::row(
226                        MainAxisSize::Min,
227                        MainAxisAlignment::Start,
228                        CrossAxisAlignment::Center,
229                    )),
230                );
231                if let Some(text) = self.text3.as_ref() {
232                    builder.text(
233                        face.clone(),
234                        text,
235                        TEXT_FONT_SIZE,
236                        Point::zero(),
237                        TextFacetOptions {
238                            horizontal_alignment: TextHorizontalAlignment::Left,
239                            vertical_alignment: TextVerticalAlignment::Bottom,
240                            color: TEXT_COLOR,
241                            ..TextFacetOptions::default()
242                        },
243                    );
244                }
245                if let Some(button_infos) = self.button_infos2.as_ref() {
246                    builder.space(size2(10.0, 40.0));
247                    buttons.append(&mut self.add_buttons(
248                        builder,
249                        button_infos,
250                        SMALL_BUTTON_FONT_SIZE,
251                    ));
252                }
253
254                builder.end_group(); // bottom button row
255                builder.end_group(); // left column contents
256                builder.end_group(); // left column space
257                builder.end_group(); // left column
258
259                let options =
260                    StackOptions { alignment: Alignment::center_left(), ..StackOptions::default() };
261                builder.start_group("right screen", Stack::with_options_ptr(options));
262                builder.start_group(
263                    "right column",
264                    Flex::with_options_ptr(FlexOptions::column(
265                        MainAxisSize::Min,
266                        MainAxisAlignment::SpaceEvenly,
267                        CrossAxisAlignment::Center,
268                    )),
269                );
270                if self.split == ScreenSplit::Wide {
271                    self.device_panel(builder, face.clone());
272                } else if self.split == ScreenSplit::Even {
273                    builder.start_group(
274                        "right columm row",
275                        Flex::with_options_ptr(FlexOptions::row(
276                            MainAxisSize::Min,
277                            MainAxisAlignment::Center,
278                            CrossAxisAlignment::Center,
279                        )),
280                    );
281                    let mut logo_size = size2(296.0, 170.0);
282                    if let Some(icon_size) = self.icon_size {
283                        logo_size = icon_size;
284                    }
285                    builder.space(size2((width2 - logo_size.width) / 2.0, 10.0));
286                    self.add_facet(builder, self.icon_name, logo_size);
287                    builder.end_group(); // right column row
288                }
289                builder.end_group(); // right column
290                builder.rectangle(size2(width2, target_size.height), BACKGROUND_GREY);
291                builder.end_group(); // right screen
292                builder.end_group(); // main screen
293            },
294        );
295        let mut scene = builder.build();
296        for button in &mut buttons {
297            button.set_focused(&mut scene, true);
298        }
299        scene.layout(target_size);
300        SceneDetails { scene, buttons }
301    }
302
303    fn add_privacy_switch(
304        &mut self,
305        buttons: &mut Buttons,
306        face: &FontFace,
307        builder: &mut SceneBuilder,
308        width1: Coord,
309    ) {
310        let ask_permission = self.permission_button_infos.is_some();
311        if let Some(text) = self.text2.as_ref() {
312            builder.space(size2(width1, AFTER_TITLE_SPACE));
313            builder.text(
314                face.clone(),
315                text,
316                TEXT_FONT_SIZE,
317                Point::zero(),
318                TextFacetOptions {
319                    horizontal_alignment: TextHorizontalAlignment::Left,
320                    vertical_alignment: TextVerticalAlignment::Top,
321                    color: TEXT_COLOR,
322                    ..TextFacetOptions::default()
323                },
324            );
325            if ask_permission {
326                builder.space(size2(width1, AFTER_TEXT_SPACE / 2.0));
327            } else {
328                builder.space(size2(width1, AFTER_TEXT_SPACE));
329            }
330        }
331        if ask_permission {
332            buttons.push(self.add_report_permission(
333                builder,
334                self.permission_button_infos.as_ref().unwrap()[0].clone(),
335                &face,
336            ));
337            builder.space(size2(width1, AFTER_TEXT_SPACE / 2.0));
338        }
339    }
340
341    fn add_buttons(
342        &self,
343        builder: &mut SceneBuilder,
344        button_infos: &ButtonInfos,
345        button_font_size: f32,
346    ) -> Buttons {
347        let mut buttons: Buttons = Vec::new();
348        if !button_infos.is_empty() {
349            builder.start_group(
350                "Button Row",
351                Flex::with_options_ptr(FlexOptions::row(
352                    MainAxisSize::Min,
353                    MainAxisAlignment::SpaceEvenly,
354                    CrossAxisAlignment::End,
355                )),
356            );
357            for button_info in button_infos {
358                buttons.push(builder.button(
359                    button_info.text,
360                    self.get_facet(button_info.icon_name, ICON_ADD_SIZE),
361                    ButtonOptions {
362                        font_size: button_font_size,
363                        shape: ButtonShape::Oval,
364                        draw_border: button_info.border,
365                        bg_fg_swapped: button_info.reversed,
366                        ..ButtonOptions::default()
367                    },
368                ));
369                builder.space(size2(BUTTON_SPACE, 10.0));
370            }
371            builder.end_group(); // Button Row
372        }
373        buttons
374    }
375
376    fn device_panel(&mut self, builder: &mut SceneBuilder, face: FontFace) {
377        builder.start_group(
378            &("G Panel Row"),
379            Flex::with_options_ptr(FlexOptions::row(
380                MainAxisSize::Min,
381                MainAxisAlignment::Start,
382                CrossAxisAlignment::Start,
383            )),
384        );
385        builder.space(size2(20.0, 10.0));
386        builder.start_group(
387            &("G Panel Column"),
388            Flex::with_options_ptr(FlexOptions::column(
389                MainAxisSize::Min,
390                MainAxisAlignment::SpaceEvenly,
391                CrossAxisAlignment::Start,
392            )),
393        );
394        builder.space(size2(10.0, 60.0));
395        builder.start_group(
396            &("QR Code Row"),
397            Flex::with_options_ptr(FlexOptions::row(
398                MainAxisSize::Min,
399                MainAxisAlignment::Start,
400                CrossAxisAlignment::Center,
401            )),
402        );
403        if let Some(qr_file) = &self.qr_file {
404            let logo = RiveFacet::new_from_file(QR_CODE_SIZE, qr_file, Some(QR_CODE))
405                .expect("cannot get QR code from QR file");
406            builder.facet(Box::new(logo));
407        }
408        builder.space(size2(16.0, 1.0));
409        builder.add_text(&face, "Scan QR code\nfor Help article", G_TEXT_FONT_SIZE);
410        builder.end_group(); // QR Code Row
411        builder.space(size2(10.0, 208.0));
412        let logo_size = size2(G_LOGO_SIZE, G_LOGO_SIZE);
413        if let Some(logo_file) = &self.logo_file {
414            let logo = RiveFacet::new_from_file(logo_size, logo_file, None)
415                .expect("cannot get logo from logo file");
416            builder.facet(Box::new(logo));
417        }
418        builder.space(size2(10.0, 8.0));
419        builder.add_text(&face, "Device information", G_TITLE_FONT_SIZE);
420        builder.add_text(&face, "(TO BE IMPLEMENTED)", G_TITLE_FONT_SIZE);
421        builder.space(size2(10.0, 8.0));
422        builder.add_text(&face, "Product: Product Name", G_TEXT_FONT_SIZE);
423        builder.add_text(&face, "Serial number: 123-456-7890", G_TEXT_FONT_SIZE);
424        builder.add_text(&face, "Build: 00.00000000.00", G_TEXT_FONT_SIZE);
425        builder.add_text(&face, "DRAM:: XXXXX", G_TEXT_FONT_SIZE);
426        builder.add_text(&face, "UFS: Unknown", G_TEXT_FONT_SIZE);
427        builder.add_text(&face, "Package version: XXXXX", G_TEXT_FONT_SIZE);
428        builder.end_group(); // G Panel Column
429        builder.end_group(); // G Panel Row
430    }
431
432    fn add_report_permission(
433        &mut self,
434        builder: &mut SceneBuilder,
435        button_info: ButtonInfo,
436        face: &FontFace,
437    ) -> Button {
438        builder.start_group(
439            &("Permissions Row"),
440            Flex::with_options_ptr(FlexOptions::row(
441                MainAxisSize::Min,
442                MainAxisAlignment::Start,
443                CrossAxisAlignment::Center,
444            )),
445        );
446        builder.add_text(&face, OPTIONAL_REPORT_TEXT, PERMISSION_FONT_SIZE);
447        let button_true = button_info.reversed;
448        #[cfg(feature = "debug_logging")]
449        println!("====== Privacy button set to {}", button_true);
450        let icon_name = if button_true { IMAGE_SWITCH_ON } else { IMAGE_SWITCH_OFF };
451        let facet = self.get_facet(Some(icon_name), IMAGE_SWITCH_SIZE);
452        let button_text = self.permission_button_infos.as_ref().unwrap()[0].text;
453        let button = builder.button(
454            button_text,
455            facet,
456            ButtonOptions { hide_text: true, bg_fg_swapped: true, ..ButtonOptions::default() },
457        );
458        builder.end_group(); // Permissions Row
459        button
460    }
461
462    fn get_facet(
463        &self,
464        artboard_name: Option<&'static str>,
465        logo_size: Size2D<f32, UnknownUnit>,
466    ) -> Option<RiveFacet> {
467        if artboard_name.is_none() {
468            return None;
469        }
470        if let Some(icon_file) = &self.icon_file {
471            Some(
472                RiveFacet::new_from_file(logo_size, icon_file, artboard_name)
473                    .expect("facet_from_file"),
474            )
475        } else {
476            None
477        }
478    }
479
480    fn add_facet(
481        &self,
482        builder: &mut SceneBuilder,
483        artboard_name: Option<&'static str>,
484        logo_size: Size2D<f32, UnknownUnit>,
485    ) {
486        if let Some(facet) = self.get_facet(artboard_name, logo_size) {
487            builder.facet(Box::new(facet));
488        }
489    }
490}
491
492pub trait SceneBuilderTextExt {
493    fn add_text(&mut self, face: &FontFace, text: &'static str, size: f32);
494}
495
496impl SceneBuilderTextExt for SceneBuilder {
497    fn add_text(&mut self, face: &FontFace, text: &'static str, size: f32) {
498        self.text(
499            face.clone(),
500            text,
501            size,
502            Point::zero(),
503            TextFacetOptions {
504                horizontal_alignment: TextHorizontalAlignment::Left,
505                color: TEXT_COLOR,
506                max_width: None,
507                ..TextFacetOptions::default()
508            },
509        );
510    }
511}
512
513impl ViewAssistant for GenericSplitViewAssistant {
514    fn resize(&mut self, _new_size: &Size) -> Result<(), Error> {
515        self.scene_details = None;
516        Ok(())
517    }
518
519    fn render(
520        &mut self,
521        render_context: &mut RenderContext,
522        ready_event: zx::Event,
523        context: &ViewAssistantContext,
524    ) -> Result<(), Error> {
525        let mut scene_details =
526            self.scene_details.take().unwrap_or_else(|| self.generic_split_scene(context));
527
528        scene_details.scene.render(render_context, ready_event, context)?;
529        self.scene_details = Some(scene_details);
530        context.request_render();
531        Ok(())
532    }
533
534    fn handle_pointer_event(
535        &mut self,
536        context: &mut ViewAssistantContext,
537        _event: &input::Event,
538        pointer_event: &input::pointer::Event,
539    ) -> Result<(), Error> {
540        if let Some(scene_details) = self.scene_details.as_mut() {
541            for button in &mut scene_details.buttons {
542                button.handle_pointer_event(&mut scene_details.scene, context, &pointer_event);
543            }
544        }
545        context.request_render();
546        Ok(())
547    }
548
549    fn handle_message(&mut self, message: Message) {
550        if let Some(button_message) = message.downcast_ref::<ButtonMessages>() {
551            match button_message {
552                ButtonMessages::Pressed(_time, button_text) => {
553                    #[cfg(feature = "debug_logging")]
554                    println!("====== Received button press: {}", button_text);
555                    self.check_if_any_button_has_been_pressed(button_text);
556                }
557            }
558        }
559    }
560}