1use 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 _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 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(); 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 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(); 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 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(); 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(); builder.end_group(); builder.end_group(); 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 builder.space(size2(140.0, MIN_SPACE));
201 builder.end_group(); 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}