Skip to main content

recovery_ui/
text_field.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, ButtonOptions, ButtonShape, SceneBuilderButtonExt};
6use crate::constants::constants::{
7    BORDER_WIDTH, ICONS_PATH, ICON_PASSWORD_INVISIBLE, ICON_PASSWORD_VISIBLE,
8    ICON_PASSWORD_VISIBLE_SIZE, MIN_SPACE, TEXT_FIELD_FONT_SIZE, TEXT_FIELD_TITLE_SIZE,
9};
10use crate::font;
11use carnelian::color::Color;
12use carnelian::render::rive::load_rive;
13use carnelian::scene::facets::{
14    FacetId, RiveFacet, SetTextMessage, TextFacetOptions, TextVerticalAlignment,
15};
16use carnelian::scene::layout::{
17    Alignment, CrossAxisAlignment, Flex, FlexOptions, MainAxisAlignment, MainAxisSize, Stack,
18    StackOptions,
19};
20use carnelian::scene::scene::{Scene, SceneBuilder};
21use carnelian::{input, Coord, Point, ViewAssistantContext};
22use derivative::Derivative;
23use euclid::{size2, Size2D, UnknownUnit};
24use rive_rs::File;
25use std::ops::Add;
26
27#[derive(PartialEq, Clone, Copy)]
28pub enum TextVisibility {
29    Always,
30    Toggleable(bool),
31}
32
33impl TextVisibility {
34    pub fn toggle(&self) -> TextVisibility {
35        if let TextVisibility::Toggleable(boolean) = self {
36            TextVisibility::Toggleable(!boolean)
37        } else {
38            TextVisibility::Always
39        }
40    }
41}
42
43#[derive(Debug, Derivative)]
44#[derivative(Default)]
45pub struct TextFieldOptions {
46    #[derivative(Default(value = "true"))]
47    pub draw_border: bool,
48    #[derivative(Default(value = "TEXT_FIELD_FONT_SIZE"))]
49    pub text_size: f32,
50    #[derivative(Default(value = "TEXT_FIELD_TITLE_SIZE"))]
51    pub title_size: f32,
52    #[derivative(Default(value = "6.0"))]
53    pub padding: f32,
54    #[derivative(Default(value = "ButtonShape::Oval"))]
55    pub shape: ButtonShape,
56}
57
58pub struct TextField {
59    title: String,
60    text: String,
61    privacy: TextVisibility,
62    text_field: FacetId,
63    button: Option<Button>,
64    // We need to keep this file open while icons are in use
65    _icon_file: Option<File>,
66}
67
68impl TextField {
69    pub fn new(
70        title: String,
71        text: String,
72        privacy: TextVisibility,
73        size: Size2D<f32, UnknownUnit>,
74        options: TextFieldOptions,
75        builder: &mut SceneBuilder,
76    ) -> Self {
77        let icon_file = Self::open_icon_file();
78        let stack_options =
79            StackOptions { alignment: Alignment::top_left(), ..StackOptions::default() };
80
81        builder.start_group("text field", Stack::with_options_ptr(stack_options));
82        builder.start_group(
83            &("title row"),
84            Flex::with_options_ptr(FlexOptions::row(
85                MainAxisSize::Min,
86                MainAxisAlignment::Start,
87                CrossAxisAlignment::Start,
88            )),
89        );
90        // TODO(b/259497403): Calculate hardcoded values from screen width
91        builder.space(size2(40.0, MIN_SPACE));
92        builder.text(
93            font::get_default_font_face().clone(),
94            &title,
95            options.title_size,
96            Point::zero(),
97            TextFacetOptions {
98                background_color: Some(Color::white()),
99                ..TextFacetOptions::default()
100            },
101        );
102        builder.end_group(); // title row
103
104        // We need a column here to push the ovals down a little
105        // so that the title text cuts the top of the oval.
106        builder.start_group(
107            "field body column",
108            Flex::with_options_ptr(FlexOptions::column(
109                MainAxisSize::Min,
110                MainAxisAlignment::Start,
111                CrossAxisAlignment::Start,
112            )),
113        );
114        builder.space(size2(MIN_SPACE, options.title_size / 2.0));
115        let stack_options =
116            StackOptions { alignment: Alignment::center_left(), ..StackOptions::default() };
117        builder.start_group("field body", Stack::with_options_ptr(stack_options));
118        builder.start_group(
119            &("Text row"),
120            Flex::with_options_ptr(FlexOptions::row(
121                MainAxisSize::Max,
122                MainAxisAlignment::Start,
123                CrossAxisAlignment::Center,
124            )),
125        );
126        // TODO(b/259497403): Calculate hardcoded values from screen width
127        builder.space(size2(35.0, MIN_SPACE));
128        let formatted_text = Self::format_text(text.clone(), privacy);
129        let text_field = builder.text(
130            font::get_default_font_face().clone(),
131            &formatted_text,
132            options.text_size,
133            Point::zero(),
134            TextFacetOptions {
135                color: Color::new(),
136                vertical_alignment: TextVerticalAlignment::Center,
137                ..TextFacetOptions::default()
138            },
139        );
140        builder.end_group(); // Text row
141
142        let button = match privacy {
143            TextVisibility::Toggleable(text_visible) => {
144                Some(Self::add_privacy_button(&icon_file, &title, text_visible, builder))
145            }
146            TextVisibility::Always => None,
147        };
148
149        // This row is necessary to align the rectangles to create a border
150        builder.start_group(
151            &("Border row"),
152            Flex::with_options_ptr(FlexOptions::row(
153                MainAxisSize::Max,
154                MainAxisAlignment::Start,
155                CrossAxisAlignment::Center,
156            )),
157        );
158        builder.space(size2(BORDER_WIDTH / 2.0, BORDER_WIDTH / 2.0));
159        let bg_size = size;
160        let corner: Coord = Coord::from((bg_size.height) * (options.shape as i32 as f32) / 100.0);
161        builder.rounded_rectangle(bg_size, corner, Color::white());
162        builder.end_group(); // Border row
163
164        let bg_size = bg_size.add(size2(BORDER_WIDTH, BORDER_WIDTH));
165        let corner: Coord = Coord::from((bg_size.height) * (options.shape as i32 as f32) / 100.0);
166        builder.rounded_rectangle(bg_size, corner, Color::new());
167        builder.end_group(); // field body
168        builder.end_group(); // field body column
169        builder.end_group(); // text field
170
171        Self { title, text, privacy, text_field, button, _icon_file: icon_file }
172    }
173
174    fn add_privacy_button(
175        icon_file: &Option<File>,
176        title: &String,
177        text_visible: bool,
178        builder: &mut SceneBuilder,
179    ) -> Button {
180        builder.start_group(
181            &("Icon row"),
182            Flex::with_options_ptr(FlexOptions::row(
183                MainAxisSize::Max,
184                MainAxisAlignment::End,
185                CrossAxisAlignment::Center,
186            )),
187        );
188
189        let button = builder.button(
190            &title,
191            Self::get_eye_icon(icon_file, text_visible),
192            ButtonOptions {
193                hide_text: true,
194                bg_fg_swapped: true,
195                shape: ButtonShape::Rounded,
196                ..ButtonOptions::default()
197            },
198        );
199        // TODO(b/259497403): Calculate hardcoded values from screen width
200        builder.space(size2(140.0, MIN_SPACE));
201        builder.end_group(); // Icon row
202        button
203    }
204
205    fn open_icon_file() -> Option<File> {
206        let icon_file = load_rive(ICONS_PATH);
207        match icon_file {
208            Ok(file) => Some(file),
209            Err(error) => {
210                eprintln!("Cannot read Rive icon file: {}", error);
211                None
212            }
213        }
214    }
215
216    fn get_eye_icon(icon_file: &Option<File>, visible: bool) -> Option<RiveFacet> {
217        match icon_file {
218            Some(icon_file) => {
219                let icon_name =
220                    if visible { ICON_PASSWORD_VISIBLE } else { ICON_PASSWORD_INVISIBLE };
221                let facet = RiveFacet::new_from_file(
222                    ICON_PASSWORD_VISIBLE_SIZE,
223                    &icon_file,
224                    Some(icon_name),
225                );
226                match facet {
227                    Ok(facet) => Some(facet),
228                    Err(error) => {
229                        eprintln!("failed to read password icon from file: {}", error);
230                        None
231                    }
232                }
233            }
234            None => None,
235        }
236    }
237
238    pub fn set_title(&mut self, title: String) {
239        self.title = title;
240    }
241
242    pub fn get_title(&self) -> &String {
243        &self.title
244    }
245
246    pub fn set_text(&mut self, text: String) {
247        self.text = text;
248    }
249
250    pub fn format_text(text: String, privacy: TextVisibility) -> String {
251        if TextVisibility::Toggleable(false) == privacy {
252            format!("{:*<1$}", "", text.len())
253        } else {
254            text
255        }
256    }
257
258    pub fn update_text(&mut self, scene: &mut Scene, text: String) {
259        self.set_text(text.clone());
260        let formatted_text = Self::format_text(text, self.privacy);
261        scene.send_message(&self.text_field, Box::new(SetTextMessage { text: formatted_text }));
262    }
263
264    pub fn set_privacy(&mut self, privacy: TextVisibility) {
265        self.privacy = privacy;
266        self.set_text(self.text.clone());
267    }
268
269    pub fn set_focused(&mut self, scene: &mut Scene, focused: bool) {
270        if let Some(button) = &mut self.button {
271            button.set_focused(scene, focused);
272        }
273    }
274
275    pub fn handle_pointer_event(
276        &mut self,
277        scene: &mut Scene,
278        context: &mut ViewAssistantContext,
279        pointer_event: &input::pointer::Event,
280    ) {
281        if let Some(button) = &mut self.button {
282            button.handle_pointer_event(scene, context, &pointer_event);
283        }
284    }
285}
286
287pub trait SceneBuilderTextFieldExt {
288    fn text_field(
289        &mut self,
290        title: String,
291        text: String,
292        privacy: TextVisibility,
293        size: Size2D<f32, UnknownUnit>,
294        options: TextFieldOptions,
295    ) -> TextField;
296}
297
298impl SceneBuilderTextFieldExt for SceneBuilder {
299    fn text_field(
300        &mut self,
301        title: String,
302        text: String,
303        privacy: TextVisibility,
304        size: Size2D<f32, UnknownUnit>,
305        options: TextFieldOptions,
306    ) -> TextField {
307        TextField::new(title, text, privacy, size, options, self)
308    }
309}