blob: 4e4ac83f688484c98f23d069764a49eb64a8c9c0 [file] [log] [blame]
// 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, ButtonOptions, ButtonShape, SceneBuilderButtonExt};
use crate::constants::constants::{
BORDER_WIDTH, ICONS_PATH, ICON_PASSWORD_INVISIBLE, ICON_PASSWORD_VISIBLE,
ICON_PASSWORD_VISIBLE_SIZE, MIN_SPACE, TEXT_FIELD_FONT_SIZE, TEXT_FIELD_TITLE_SIZE,
};
use crate::font;
use carnelian::color::Color;
use carnelian::render::rive::load_rive;
use carnelian::scene::facets::{
FacetId, RiveFacet, SetTextMessage, TextFacetOptions, TextVerticalAlignment,
};
use carnelian::scene::layout::{
Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, Stack,
StackOptions,
};
use carnelian::scene::scene::{Scene, SceneBuilder};
use carnelian::{input, Coord, Point, ViewAssistantContext};
use derivative::Derivative;
use euclid::{size2, Size2D, UnknownUnit};
use rive_rs::File;
use std::ops::Add;
#[derive(PartialEq, Clone, Copy)]
pub enum TextVisibility {
Always,
Toggleable(bool),
}
impl TextVisibility {
pub fn toggle(&self) -> TextVisibility {
if let TextVisibility::Toggleable(boolean) = self {
TextVisibility::Toggleable(!boolean)
} else {
TextVisibility::Always
}
}
}
#[derive(Debug, Derivative)]
#[derivative(Default)]
pub struct TextFieldOptions {
#[derivative(Default(value = "true"))]
pub draw_border: bool,
#[derivative(Default(value = "TEXT_FIELD_FONT_SIZE"))]
pub text_size: f32,
#[derivative(Default(value = "TEXT_FIELD_TITLE_SIZE"))]
pub title_size: f32,
#[derivative(Default(value = "6.0"))]
pub padding: f32,
#[derivative(Default(value = "ButtonShape::Oval"))]
pub shape: ButtonShape,
}
pub struct TextField {
title: String,
text: String,
privacy: TextVisibility,
text_field: FacetId,
button: Option<Button>,
// We need to keep this file open while icons are in use
_icon_file: Option<File>,
}
impl TextField {
pub fn new(
title: String,
text: String,
privacy: TextVisibility,
size: Size2D<f32, UnknownUnit>,
options: TextFieldOptions,
builder: &mut SceneBuilder,
) -> Self {
let icon_file = Self::open_icon_file();
let stack_options =
StackOptions { alignment: Alignment::top_left(), ..StackOptions::default() };
builder.start_group("text field", Stack::with_options_ptr(stack_options));
builder.start_group(
&("title row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Start,
)),
);
// TODO(b/259497403): Calculate hardcoded values from screen width
builder.space(size2(40.0, MIN_SPACE));
builder.text(
font::get_default_font_face().clone(),
&title,
options.title_size,
Point::zero(),
TextFacetOptions {
background_color: Some(Color::white()),
..TextFacetOptions::default()
},
);
builder.end_group(); // title row
// We need a column here to push the ovals down a little
// so that the title text cuts the top of the oval.
builder.start_group(
"field body column",
Flex::with_options_ptr(FlexOptions::column(
MainAxisSize::Min,
MainAxisAlignment::Start,
CrossAxisAlignment::Start,
)),
);
builder.space(size2(MIN_SPACE, options.title_size / 2.0));
let stack_options =
StackOptions { alignment: Alignment::center_left(), ..StackOptions::default() };
builder.start_group("field body", Stack::with_options_ptr(stack_options));
builder.start_group(
&("Text row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Max,
MainAxisAlignment::Start,
CrossAxisAlignment::Center,
)),
);
// TODO(b/259497403): Calculate hardcoded values from screen width
builder.space(size2(35.0, MIN_SPACE));
let formatted_text = Self::format_text(text.clone(), privacy);
let text_field = builder.text(
font::get_default_font_face().clone(),
&formatted_text,
options.text_size,
Point::zero(),
TextFacetOptions {
color: Color::new(),
vertical_alignment: TextVerticalAlignment::Center,
..TextFacetOptions::default()
},
);
builder.end_group(); // Text row
let button = match privacy {
TextVisibility::Toggleable(text_visible) => {
Some(Self::add_privacy_button(&icon_file, &title, text_visible, builder))
}
TextVisibility::Always => None,
};
// This row is necessary to align the rectangles to create a border
builder.start_group(
&("Border row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Max,
MainAxisAlignment::Start,
CrossAxisAlignment::Center,
)),
);
builder.space(size2(BORDER_WIDTH / 2.0, BORDER_WIDTH / 2.0));
let bg_size = size;
let corner: Coord =
Coord::from((bg_size.height) * (options.shape.clone() as i32 as f32) / 100.0);
builder.rounded_rectangle(bg_size, corner, Color::white());
builder.end_group(); // Border row
let bg_size = bg_size.add(size2(BORDER_WIDTH, BORDER_WIDTH));
let corner: Coord =
Coord::from((bg_size.height) * (options.shape.clone() as i32 as f32) / 100.0);
builder.rounded_rectangle(bg_size, corner, Color::new());
builder.end_group(); // field body
builder.end_group(); // field body column
builder.end_group(); // text field
Self { title, text, privacy, text_field, button, _icon_file: icon_file }
}
fn add_privacy_button(
icon_file: &Option<File>,
title: &String,
text_visible: bool,
builder: &mut SceneBuilder,
) -> Button {
builder.start_group(
&("Icon row"),
Flex::with_options_ptr(FlexOptions::row(
MainAxisSize::Max,
MainAxisAlignment::End,
CrossAxisAlignment::Center,
)),
);
let button = builder.button(
&title,
Self::get_eye_icon(icon_file, text_visible),
ButtonOptions {
hide_text: true,
bg_fg_swapped: true,
shape: ButtonShape::Rounded,
..ButtonOptions::default()
},
);
// TODO(b/259497403): Calculate hardcoded values from screen width
builder.space(size2(140.0, MIN_SPACE));
builder.end_group(); // Icon row
button
}
fn open_icon_file() -> Option<File> {
let icon_file = load_rive(ICONS_PATH);
match icon_file {
Ok(file) => Some(file),
Err(error) => {
eprintln!("Cannot read Rive icon file: {}", error);
None
}
}
}
fn get_eye_icon(icon_file: &Option<File>, visible: bool) -> Option<RiveFacet> {
match icon_file {
Some(icon_file) => {
let icon_name =
if visible { ICON_PASSWORD_VISIBLE } else { ICON_PASSWORD_INVISIBLE };
let facet = RiveFacet::new_from_file(
ICON_PASSWORD_VISIBLE_SIZE,
&icon_file,
Some(icon_name),
);
match facet {
Ok(facet) => Some(facet),
Err(error) => {
eprintln!("failed to read password icon from file: {}", error);
None
}
}
}
None => None,
}
}
pub fn set_title(&mut self, title: String) {
self.title = title;
}
pub fn get_title(&self) -> &String {
&self.title
}
pub fn set_text(&mut self, text: String) {
self.text = text.clone();
}
pub fn format_text(text: String, privacy: TextVisibility) -> String {
if TextVisibility::Toggleable(false) == privacy {
format!("{:*<1$}", "", text.len())
} else {
text
}
}
pub fn update_text(&mut self, scene: &mut Scene, text: String) {
self.set_text(text.clone());
let formatted_text = Self::format_text(text, self.privacy);
scene.send_message(&self.text_field, Box::new(SetTextMessage { text: formatted_text }));
}
pub fn set_privacy(&mut self, privacy: TextVisibility) {
self.privacy = privacy;
self.set_text(self.text.clone());
}
pub fn set_focused(&mut self, scene: &mut Scene, focused: bool) {
if let Some(button) = &mut self.button {
button.set_focused(scene, focused);
}
}
pub fn handle_pointer_event(
&mut self,
scene: &mut Scene,
context: &mut ViewAssistantContext,
pointer_event: &input::pointer::Event,
) {
if let Some(button) = &mut self.button {
button.handle_pointer_event(scene, context, &pointer_event);
}
}
}
pub trait SceneBuilderTextFieldExt {
fn text_field(
&mut self,
title: String,
text: String,
privacy: TextVisibility,
size: Size2D<f32, UnknownUnit>,
options: TextFieldOptions,
) -> TextField;
}
impl SceneBuilderTextFieldExt for SceneBuilder {
fn text_field(
&mut self,
title: String,
text: String,
privacy: TextVisibility,
size: Size2D<f32, UnknownUnit>,
options: TextFieldOptions,
) -> TextField {
TextField::new(title, text, privacy, size, options, self)
}
}