blob: 17afc7103a7702de69ac77c8d1d4104bfcf066ed [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 crate::button::{Button, ButtonMessages, ButtonOptions, ButtonShape, SceneBuilderButtonExt};
use crate::constants::constants::*;
use crate::font::get_default_font_face;
use anyhow::Error;
use carnelian::drawing::FontFace;
use carnelian::render::rive::load_rive;
use carnelian::render::Context as RenderContext;
use carnelian::scene::facets::{
RiveFacet, TextFacetOptions, TextHorizontalAlignment, TextVerticalAlignment,
};
use carnelian::scene::layout::{
Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, Stack,
StackOptions,
};
use carnelian::scene::scene::{Scene, SceneBuilder};
use carnelian::{
input, make_message, AppSender, Coord, Message, MessageTarget, Point, Size, ViewAssistant,
ViewAssistantContext, ViewKey,
};
use euclid::{size2, Size2D, UnknownUnit};
use recovery_util::ota::state_machine::Event;
use rive_rs::File;
#[derive(Debug, Clone)]
pub struct ButtonInfo {
pub text: &'static str,
pub icon_name: Option<&'static str>,
pub border: bool,
pub reversed: bool,
pub event: Event,
}
impl ButtonInfo {
pub fn new(
text: &'static str,
icon_name: Option<&'static str>,
border: bool,
reversed: bool,
event: Event,
) -> Self {
Self { text, icon_name, border, reversed, event }
}
}
type Buttons = Vec<Button>;
type ButtonInfos = Vec<ButtonInfo>;
pub struct SceneDetails {
pub(crate) scene: Scene,
buttons: Buttons,
}
pub struct GenericSplitViewAssistant {
app_sender: AppSender,
view_key: ViewKey,
split: ScreenSplit,
text1: Option<String>,
text2: Option<String>,
text3: Option<String>,
button_infos1: Option<ButtonInfos>,
button_infos2: Option<ButtonInfos>,
permission_button_infos: Option<ButtonInfos>,
icon_name: Option<&'static str>,
icon_size: Option<Size2D<f32, UnknownUnit>>,
scene_details: Option<SceneDetails>,
icon_file: Option<File>,
logo_file: Option<File>,
qr_file: Option<File>,
}
impl GenericSplitViewAssistant {
// TODO(b/259793406) Consolidate into a struct, rename and document arguments to this function
pub fn new(
app_sender: AppSender,
view_key: ViewKey,
split: ScreenSplit,
text1: Option<String>,
text2: Option<String>,
text3: Option<String>,
button_infos1: Option<ButtonInfos>,
button_infos2: Option<ButtonInfos>,
permission_button_infos: Option<ButtonInfos>,
icon_name: Option<&'static str>,
icon_size: Option<Size2D<f32, UnknownUnit>>,
) -> Result<GenericSplitViewAssistant, Error> {
#[cfg(feature = "debug_logging")]
println!("====== New generic view");
let icon_file = load_rive(ICONS_PATH).ok();
let logo_file = load_rive(LOGO_PATH).ok();
let qr_file = load_rive(QR_PATH).ok();
Ok(GenericSplitViewAssistant {
app_sender: app_sender,
view_key,
split,
text1,
text2,
text3,
button_infos1,
button_infos2,
permission_button_infos,
icon_name,
icon_size,
scene_details: None,
icon_file,
logo_file,
qr_file,
})
}
fn check_if_a_button_in_array_has_been_pressed(
&self,
text: &String,
button_infos: &ButtonInfos,
) -> bool {
for button_info in button_infos {
// Generate the event that this button press has caused
if button_info.text == text {
#[cfg(feature = "debug_logging")]
println!("====== Generating event: {:?}", button_info.event);
self.app_sender.queue_message(
MessageTarget::View(self.view_key),
make_message(button_info.event.clone()),
);
return true;
}
}
false
}
fn check_if_any_button_has_been_pressed(&self, text: &String) {
let mut found = false;
if let Some(button_infos) = self.button_infos1.as_ref() {
found = self.check_if_a_button_in_array_has_been_pressed(text, button_infos);
}
if !found {
if let Some(button_infos) = self.button_infos2.as_ref() {
found = self.check_if_a_button_in_array_has_been_pressed(text, button_infos);
}
}
if !found {
if let Some(button_infos) = self.permission_button_infos.as_ref() {
self.check_if_a_button_in_array_has_been_pressed(text, button_infos);
}
}
}
fn generic_split_scene(&mut self, context: &ViewAssistantContext) -> SceneDetails {
let target_size = context.size;
let min_dimension = target_size.width.min(target_size.height);
let _padding = (min_dimension / 20.0).ceil().max(8.0);
let mut builder = SceneBuilder::new().background_color(BACKGROUND_WHITE);
let mut buttons: Buttons = Vec::new();
let split = self.split.as_percent();
let face = get_default_font_face();
builder.group().column().max_size().main_align(MainAxisAlignment::SpaceEvenly).contents(
|builder| {
builder.start_group(
"main screen",
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Max,
MainAxisAlignment::SpaceEvenly,
CrossAxisAlignment::Start,
)),
);
builder.start_group(
"left column",
Flex::with_options_ptr(FlexOptions::column(
MainAxisSize::Min,
MainAxisAlignment::SpaceEvenly,
CrossAxisAlignment::Start,
)),
);
builder.start_group(
"left row for margin space",
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Start,
)),
);
builder.space(size2(LEFT_MARGIN_SPACE, 10.0));
builder.start_group(
"left column contents",
Flex::with_options_ptr(FlexOptions::column(
MainAxisSize::Min,
MainAxisAlignment::SpaceEvenly,
CrossAxisAlignment::Start,
)),
);
let width1 = target_size.width * split;
let width2 = target_size.width - width1;
builder.space(size2(width1, TOP_SPACE));
if let Some(text) = self.text1.as_ref() {
builder.text(
face.clone(),
text,
TITLE_FONT_SIZE,
Point::zero(),
TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Left,
vertical_alignment: TextVerticalAlignment::Top,
color: TITLE_COLOR,
..TextFacetOptions::default()
},
);
}
self.add_privacy_switch(&mut buttons, &face, builder, width1);
if let Some(button_infos) = self.button_infos1.as_ref() {
buttons.append(&mut self.add_buttons(
builder,
button_infos,
LARGE_BUTTON_FONT_SIZE,
));
}
builder.space(size2(10.0, AFTER_MAIN_BUTTON_SPACE));
builder.start_group(
"bottom button row",
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Center,
)),
);
if let Some(text) = self.text3.as_ref() {
builder.text(
face.clone(),
text,
TEXT_FONT_SIZE,
Point::zero(),
TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Left,
vertical_alignment: TextVerticalAlignment::Bottom,
color: TEXT_COLOR,
..TextFacetOptions::default()
},
);
}
if let Some(button_infos) = self.button_infos2.as_ref() {
builder.space(size2(10.0, 40.0));
buttons.append(&mut self.add_buttons(
builder,
button_infos,
SMALL_BUTTON_FONT_SIZE,
));
}
builder.end_group(); // bottom button row
builder.end_group(); // left column contents
builder.end_group(); // left column space
builder.end_group(); // left column
let options =
StackOptions { alignment: Alignment::center_left(), ..StackOptions::default() };
builder.start_group("right screen", Stack::with_options_ptr(options));
builder.start_group(
"right column",
Flex::with_options_ptr(FlexOptions::column(
MainAxisSize::Min,
MainAxisAlignment::SpaceEvenly,
CrossAxisAlignment::Center,
)),
);
if self.split == ScreenSplit::Wide {
self.device_panel(builder, face.clone());
} else if self.split == ScreenSplit::Even {
builder.start_group(
"right columm row",
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Center,
CrossAxisAlignment::Center,
)),
);
let mut logo_size = size2(296.0, 170.0);
if let Some(icon_size) = self.icon_size {
logo_size = icon_size;
}
builder.space(size2((width2 - logo_size.width) / 2.0, 10.0));
self.add_facet(builder, self.icon_name, logo_size);
builder.end_group(); // right column row
}
builder.end_group(); // right column
builder.rectangle(size2(width2, target_size.height), BACKGROUND_GREY);
builder.end_group(); // right screen
builder.end_group(); // main screen
},
);
let mut scene = builder.build();
for button in &mut buttons {
button.set_focused(&mut scene, true);
}
scene.layout(target_size);
SceneDetails { scene, buttons }
}
fn add_privacy_switch(
&mut self,
buttons: &mut Buttons,
face: &FontFace,
builder: &mut SceneBuilder,
width1: Coord,
) {
let ask_permission = self.permission_button_infos.is_some();
if let Some(text) = self.text2.as_ref() {
builder.space(size2(width1, AFTER_TITLE_SPACE));
builder.text(
face.clone(),
text,
TEXT_FONT_SIZE,
Point::zero(),
TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Left,
vertical_alignment: TextVerticalAlignment::Top,
color: TEXT_COLOR,
..TextFacetOptions::default()
},
);
if ask_permission {
builder.space(size2(width1, AFTER_TEXT_SPACE / 2.0));
} else {
builder.space(size2(width1, AFTER_TEXT_SPACE));
}
}
if ask_permission {
buttons.push(self.add_report_permission(
builder,
self.permission_button_infos.as_ref().unwrap()[0].clone(),
&face,
));
builder.space(size2(width1, AFTER_TEXT_SPACE / 2.0));
}
}
fn add_buttons(
&self,
builder: &mut SceneBuilder,
button_infos: &ButtonInfos,
button_font_size: f32,
) -> Buttons {
let mut buttons: Buttons = Vec::new();
if !button_infos.is_empty() {
builder.start_group(
"Button Row",
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::SpaceEvenly,
CrossAxisAlignment::End,
)),
);
for button_info in button_infos {
buttons.push(builder.button(
button_info.text,
self.get_facet(button_info.icon_name, ICON_ADD_SIZE),
ButtonOptions {
font_size: button_font_size,
shape: ButtonShape::Oval,
draw_border: button_info.border,
bg_fg_swapped: button_info.reversed,
..ButtonOptions::default()
},
));
builder.space(size2(BUTTON_SPACE, 10.0));
}
builder.end_group(); // Button Row
}
buttons
}
fn device_panel(&mut self, builder: &mut SceneBuilder, face: FontFace) {
builder.start_group(
&("G Panel Row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Start,
)),
);
builder.space(size2(20.0, 10.0));
builder.start_group(
&("G Panel Column"),
Flex::with_options_ptr(FlexOptions::column(
MainAxisSize::Min,
MainAxisAlignment::SpaceEvenly,
CrossAxisAlignment::Start,
)),
);
builder.space(size2(10.0, 60.0));
builder.start_group(
&("QR Code Row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Center,
)),
);
if let Some(qr_file) = &self.qr_file {
let logo = RiveFacet::new_from_file(QR_CODE_SIZE, qr_file, Some(QR_CODE))
.expect("cannot get QR code from QR file");
builder.facet(Box::new(logo));
}
builder.space(size2(16.0, 1.0));
builder.add_text(&face, "Scan QR code\nfor Help article", G_TEXT_FONT_SIZE);
builder.end_group(); // QR Code Row
builder.space(size2(10.0, 208.0));
let logo_size = size2(G_LOGO_SIZE, G_LOGO_SIZE);
if let Some(logo_file) = &self.logo_file {
let logo = RiveFacet::new_from_file(logo_size, logo_file, None)
.expect("cannot get logo from logo file");
builder.facet(Box::new(logo));
}
builder.space(size2(10.0, 8.0));
builder.add_text(&face, "Device information", G_TITLE_FONT_SIZE);
builder.add_text(&face, "(TO BE IMPLEMENTED)", G_TITLE_FONT_SIZE);
builder.space(size2(10.0, 8.0));
builder.add_text(&face, "Product: Product Name", G_TEXT_FONT_SIZE);
builder.add_text(&face, "Serial number: 123-456-7890", G_TEXT_FONT_SIZE);
builder.add_text(&face, "Build: 00.00000000.00", G_TEXT_FONT_SIZE);
builder.add_text(&face, "DRAM:: XXXXX", G_TEXT_FONT_SIZE);
builder.add_text(&face, "UFS: Unknown", G_TEXT_FONT_SIZE);
builder.add_text(&face, "Package version: XXXXX", G_TEXT_FONT_SIZE);
builder.end_group(); // G Panel Column
builder.end_group(); // G Panel Row
}
fn add_report_permission(
&mut self,
builder: &mut SceneBuilder,
button_info: ButtonInfo,
face: &FontFace,
) -> Button {
builder.start_group(
&("Permissions Row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Center,
)),
);
builder.add_text(&face, OPTIONAL_REPORT_TEXT, PERMISSION_FONT_SIZE);
let button_true = button_info.reversed;
#[cfg(feature = "debug_logging")]
println!("====== Privacy button set to {}", button_true);
let icon_name = if button_true { IMAGE_SWITCH_ON } else { IMAGE_SWITCH_OFF };
let facet = self.get_facet(Some(icon_name), IMAGE_SWITCH_SIZE);
let button_text = self.permission_button_infos.as_ref().unwrap()[0].text;
let button = builder.button(
button_text,
facet,
ButtonOptions { hide_text: true, bg_fg_swapped: true, ..ButtonOptions::default() },
);
builder.end_group(); // Permissions Row
button
}
fn get_facet(
&self,
artboard_name: Option<&'static str>,
logo_size: Size2D<f32, UnknownUnit>,
) -> Option<RiveFacet> {
if artboard_name.is_none() {
return None;
}
if let Some(icon_file) = &self.icon_file {
Some(
RiveFacet::new_from_file(logo_size, icon_file, artboard_name)
.expect("facet_from_file"),
)
} else {
None
}
}
fn add_facet(
&self,
builder: &mut SceneBuilder,
artboard_name: Option<&'static str>,
logo_size: Size2D<f32, UnknownUnit>,
) {
if let Some(facet) = self.get_facet(artboard_name, logo_size) {
builder.facet(Box::new(facet));
}
}
}
pub trait SceneBuilderTextExt {
fn add_text(&mut self, face: &FontFace, text: &'static str, size: f32);
}
impl SceneBuilderTextExt for SceneBuilder {
fn add_text(&mut self, face: &FontFace, text: &'static str, size: f32) {
self.text(
face.clone(),
text,
size,
Point::zero(),
TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Left,
color: TEXT_COLOR,
max_width: None,
..TextFacetOptions::default()
},
);
}
}
impl ViewAssistant for GenericSplitViewAssistant {
fn resize(&mut self, _new_size: &Size) -> Result<(), Error> {
self.scene_details = None;
Ok(())
}
fn render(
&mut self,
render_context: &mut RenderContext,
ready_event: zx::Event,
context: &ViewAssistantContext,
) -> Result<(), Error> {
let mut scene_details =
self.scene_details.take().unwrap_or_else(|| self.generic_split_scene(context));
scene_details.scene.render(render_context, ready_event, context)?;
self.scene_details = Some(scene_details);
context.request_render();
Ok(())
}
fn handle_pointer_event(
&mut self,
context: &mut ViewAssistantContext,
_event: &input::Event,
pointer_event: &input::pointer::Event,
) -> Result<(), Error> {
if let Some(scene_details) = self.scene_details.as_mut() {
for button in &mut scene_details.buttons {
button.handle_pointer_event(&mut scene_details.scene, context, &pointer_event);
}
}
context.request_render();
Ok(())
}
fn handle_message(&mut self, message: Message) {
if let Some(button_message) = message.downcast_ref::<ButtonMessages>() {
match button_message {
ButtonMessages::Pressed(_time, button_text) => {
#[cfg(feature = "debug_logging")]
println!("====== Received button press: {}", button_text);
self.check_if_any_button_has_been_pressed(button_text);
}
}
}
}
}