1use crate::primitives::TemplatedControlImpl;
2use crate::utils::{ClipboardHelper, MaskedTextProvider};
3use ferroui_base::utilities::CultureInfo;
4use crate::{ControlImpl, TextBox, TextBoxImpl, TextBoxImplExt, TopLevel};
5use ferroui_base::input::platform::{ClipboardErrorKind, ClipboardExtensions};
6use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
7use std::any::Any;
8use ferroui_base::input::{FocusChangedEventArgs, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyGesture, TextInputEventArgs};
9use ferroui_base::interactivity::InteractiveImpl;
10use ferroui_base::layout::LayoutableImpl;
11use ferroui_base::{
12 ferro_class, ferro_impl_classes, ferro_property, instantiate, DirectProperty, FerroObject, FerroObjectImpl,
13 FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StaticType, StyledElementImpl,
14 StyledProperty, StyledPropertyMetadata, StyledPropertyOptions, TypeInfo, VisualImpl,
15};
16use std::cell::{Cell, RefCell};
17use std::rc::Rc;
18
19#[repr(C)]
21pub struct MaskedTextBox {
22 base: TextBox,
23 mask_provider: RefCell<Option<Rc<RefCell<MaskedTextProvider>>>>,
24 ignore_text_changes: Cell<bool>,
25}
26
27ferro_class!(MaskedTextBox: TextBox);
28ferroui_base::ferro_class_info!(MaskedTextBox { new: MaskedTextBox::new });
29
30ferro_impl_classes!(MaskedTextBox: VisualImpl, LayoutableImpl, InteractiveImpl, ControlImpl, TemplatedControlImpl);
31
32impl FerroObjectImpl for MaskedTextBox {
33 fn constructed(this: &Self) {
34 Self::parent_constructed(this);
36 }
37
38 fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
39 let update_mask_provider = || {
40 let mask = this.mask();
41 let mut provider = match MaskedTextProvider::new_full(
42 mask.as_deref().unwrap_or(""),
43 this.culture(),
44 true,
45 this.prompt_char(),
46 this.password_char(),
47 this.ascii_only(),
48 ) {
49 Ok(provider) => provider,
50 Err(error) => panic!("{error}"),
51 };
52 provider.set_reset_on_space(this.reset_on_space());
53 provider.set_reset_on_prompt(this.reset_on_prompt());
54
55 let provider = Rc::new(RefCell::new(provider));
56 *this.mask_provider.borrow_mut() = Some(provider.clone());
57
58 if let Some(text) = this.text() {
59 provider.borrow_mut().set(&text);
60 }
61 this.refresh_text(Some(&provider), 0, false);
62 };
63
64 let property = change.property();
65 let provider = this.mask_provider();
66
67 if property == Self::mask_property().as_property() {
68 update_mask_provider();
69
70 if let Some(mask) = this.mask().filter(|mask| !mask.is_empty()) {
71 for c in mask.chars() {
72 if !MaskedTextProvider::is_valid_mask_char(c) {
73 panic!("Specified mask contains characters that are not valid.");
74 }
75 }
76 }
77 } else if property == TextBox::password_char_property().as_property() {
78 if provider.is_some_and(|provider| provider.borrow().password_char() != this.password_char()) {
79 update_mask_provider();
80 }
81 } else if property == Self::prompt_char_property().as_property() {
82 if provider.is_some_and(|provider| provider.borrow().prompt_char() != this.prompt_char()) {
83 update_mask_provider();
84 }
85 } else if property == Self::reset_on_prompt_property().as_property() {
86 if let Some(provider) = provider {
87 provider.borrow_mut().set_reset_on_prompt(change.get_new_value::<bool>());
88 }
89 } else if property == Self::reset_on_space_property().as_property() {
90 if let Some(provider) = provider {
91 provider.borrow_mut().set_reset_on_space(change.get_new_value::<bool>());
92 }
93 } else if (property == Self::ascii_only_property().as_property()
94 && provider.as_ref().is_some_and(|provider| provider.borrow().ascii_only() != this.ascii_only()))
95 || (property == Self::culture_property().as_property()
96 && provider.as_ref().is_some_and(|provider| Some(provider.borrow().culture()) != this.culture().as_ref()))
97 {
98 update_mask_provider();
99 }
100
101 Self::parent_on_property_changed(this, change);
102 }
103}
104
105impl StyledElementImpl for MaskedTextBox {
106 fn style_key_override(_this: &Self) -> &'static TypeInfo {
107 <TextBox as StaticType>::TYPE
108 }
109}
110
111impl InputElementImpl for MaskedTextBox {
112 fn on_got_focus(this: &Self, e: &FocusChangedEventArgs) {
113 if this.hide_prompt_on_leave() {
114 if let Some(provider) = this.mask_provider() {
115 let text = provider.borrow().to_display_string();
116 this.set_text_from_internal_synchronization(Some(text));
117 }
118 }
119 Self::parent_on_got_focus(this, e);
120 }
121
122 fn on_key_down(this: &Self, e: &KeyEventArgs) {
132 let Some(provider) = this.mask_provider() else {
133 Self::parent_on_key_down(this, e);
134 return;
135 };
136
137 let keymap = this.get_platform_settings().map(|settings| settings.hotkey_configuration());
138
139 let matches = |gestures: &[KeyGesture]| gestures.iter().any(|g| g.matches(Some(e)));
140
141 if keymap.is_some_and(|keymap| matches(&keymap.paste)) {
142 let Some(clipboard) = TopLevel::get_top_level(Some(this)).and_then(|top_level| top_level.clipboard())
143 else {
144 return;
145 };
146
147 let target = this.to_ref();
148 let completed: Rc<Cell<bool>> = Rc::new(Cell::new(false));
152 let completed_in_operation = completed.clone();
153
154 ClipboardHelper::start(async move {
155 let mut text: Option<String> = None;
156 match clipboard.try_get_text_async().await {
157 Ok(value) => text = value,
158 Err(ex) if ex.kind() == ClipboardErrorKind::Timeout => {
159 }
161 Err(uex) if uex.kind() == ClipboardErrorKind::AccessDenied => {
162 if let Some(logger) = Logger::try_get(LogEventLevel::Warning, LogArea::CONTROL) {
163 let source: &dyn Any = ⌖
164 logger.log_with_values(
165 Some(source),
166 "Failed to read text from clipboard: {Error}",
167 &[&uex],
168 );
169 }
170 }
171 Err(ex) => return Err(ex),
172 }
173
174 let Some(text) = text else {
175 return Ok(());
176 };
177
178 let Some(provider) = target.mask_provider() else {
181 return Ok(());
182 };
183
184 for item in text.chars() {
185 let mut index = target.get_next_character_position(target.caret_index());
186 let inserted = provider.borrow_mut().insert_at_char(item, index);
187 if inserted {
188 index += 1;
189 target.set_current_value(TextBox::caret_index_property(), index);
190 }
191 }
192
193 let text = provider.borrow().to_display_string();
194 target.set_text_from_edit(Some(text));
195 completed_in_operation.set(true);
196 Ok(())
197 });
198
199 if completed.get() {
200 e.set_handled(true);
201 }
202 return;
203 }
204
205 if e.key != Key::Back {
206 Self::parent_on_key_down(this, e);
207 }
208
209 match e.key {
210 Key::Delete => {
211 if this.text_length().is_some_and(|length| this.caret_index() < length) {
212 let removed = provider.borrow_mut().remove_at(this.caret_index());
213 if removed {
214 this.refresh_text(Some(&provider), this.caret_index(), true);
215 }
216
217 e.set_handled(true);
218 }
219 }
220 Key::Space => {
221 let reset_on_space = provider.borrow().reset_on_space();
222 if !reset_on_space || this.selected_text().is_empty() {
223 let inserted = provider.borrow_mut().insert_at(" ", this.caret_index());
224 if inserted {
225 this.refresh_text(Some(&provider), this.caret_index(), true);
226 }
227 }
228
229 e.set_handled(true);
230 }
231 Key::Back => {
232 if this.caret_index() > 0 {
233 provider.borrow_mut().remove_at(this.caret_index() - 1);
234 }
235 this.refresh_text(Some(&provider), this.caret_index() - 1, true);
236 e.set_handled(true);
237 }
238 _ => {}
239 }
240 }
241
242 fn on_lost_focus(this: &Self, e: &FocusChangedEventArgs) {
243 if this.hide_prompt_on_leave() {
244 if let Some(provider) = this.mask_provider() {
245 let text = provider.borrow().to_string_include(!this.hide_prompt_on_leave(), true);
246 this.set_text_from_internal_synchronization(Some(text));
247 }
248 }
249 Self::parent_on_lost_focus(this, e);
250 }
251
252 fn on_text_input(this: &Self, e: &TextInputEventArgs) {
253 this.ignore_text_changes.set(true);
254
255 struct Restore<'a>(&'a Cell<bool>);
257
258 impl Drop for Restore<'_> {
259 fn drop(&mut self) {
260 self.0.set(false);
261 }
262 }
263
264 let _restore = Restore(&this.ignore_text_changes);
265
266 if this.is_read_only() {
267 e.set_handled(true);
268 Self::parent_on_text_input(this, e);
269 return;
270 }
271 let Some(provider) = this.mask_provider() else {
272 Self::parent_on_text_input(this, e);
273 return;
274 };
275
276 let (reset_on_space, reset_on_prompt, prompt_char) = {
277 let provider = provider.borrow();
278 (provider.reset_on_space(), provider.reset_on_prompt(), provider.prompt_char())
279 };
280 let text = e.text.as_deref();
281 let mut prompt_buffer = [0u8; 4];
282 let prompt: &str = prompt_char.encode_utf8(&mut prompt_buffer);
283
284 if (reset_on_space && text == Some(" ") || reset_on_prompt && text == Some(prompt))
285 && !this.selected_text().is_empty()
286 {
287 let (selection_start, selection_end) = (this.selection_start(), this.selection_end());
288 let removed = if selection_start > selection_end {
289 provider.borrow_mut().remove_at_range(selection_end, selection_start - 1)
290 } else {
291 provider.borrow_mut().remove_at_range(selection_start, selection_end - 1)
292 };
293 if removed {
294 this.set_selected_text(Some(""));
295 }
296 }
297
298 if this.text_length().is_some_and(|length| this.caret_index() < length) {
299 this.set_current_value(
300 TextBox::caret_index_property(),
301 this.get_next_character_position(this.caret_index()),
302 );
303
304 let input = text.expect("the text of a text input cannot be null");
305 let inserted = provider.borrow_mut().insert_at(input, this.caret_index());
306 if inserted {
307 this.set_caret_index(this.caret_index() + 1);
308 }
309 let next_pos = this.get_next_character_position(this.caret_index());
310 if next_pos != 0 && this.text_length() != Some(this.caret_index()) {
311 this.set_current_value(TextBox::caret_index_property(), next_pos);
312 }
313 }
314
315 this.refresh_text(Some(&provider), this.caret_index(), true);
316
317 e.set_handled(true);
318
319 Self::parent_on_text_input(this, e);
320 }
321}
322
323impl TextBoxImpl for MaskedTextBox {
324 fn coerce_text(this: &Self, mut text: Option<String>) -> Option<String> {
325 if !this.ignore_text_changes.get() {
326 if let Some(provider) = this.mask_provider() {
327 let mut provider = provider.borrow_mut();
328 match text.as_deref() {
329 None | Some("") => provider.clear(),
330 Some(text) => {
331 provider.set(text);
332 }
333 }
334
335 text = Some(provider.to_display_string());
336 }
337 }
338
339 Self::parent_coerce_text(this, text)
340 }
341}
342
343ferroui_base::ferro_properties! { impl MaskedTextBox {
344 ferro_property!(
345 pub fn ascii_only_property() -> StyledProperty<bool> {
347 FerroProperty::register::<MaskedTextBox, _>("AsciiOnly", false)
348 }
349 );
350
351 ferro_property!(
352 pub fn culture_property() -> StyledProperty<Option<CultureInfo>> {
354 FerroProperty::register::<MaskedTextBox, _>("Culture", Some(CultureInfo::current_culture()))
355 }
356 );
357
358 ferro_property!(
359 pub fn hide_prompt_on_leave_property() -> StyledProperty<bool> {
361 FerroProperty::register::<MaskedTextBox, _>("HidePromptOnLeave", false)
362 }
363 );
364
365 ferro_property!(
366 pub fn mask_completed_property() -> DirectProperty<MaskedTextBox, Option<bool>> {
368 FerroProperty::register_direct::<MaskedTextBox, _>("MaskCompleted", |o| o.mask_completed(), None, None)
369 }
370 );
371
372 ferro_property!(
373 pub fn mask_full_property() -> DirectProperty<MaskedTextBox, Option<bool>> {
375 FerroProperty::register_direct::<MaskedTextBox, _>("MaskFull", |o| o.mask_full(), None, None)
376 }
377 );
378
379 ferro_property!(
380 pub fn mask_property() -> StyledProperty<Option<String>> {
382 FerroProperty::register::<MaskedTextBox, _>("Mask", Some(String::new()))
383 }
384 );
385
386 ferro_property!(
387 pub fn prompt_char_property() -> StyledProperty<char> {
389 FerroProperty::register_with::<MaskedTextBox, _>(
390 "PromptChar",
391 StyledPropertyOptions::new('_').coerce(Self::coerce_prompt_char),
392 )
393 }
394 );
395
396 ferro_property!(
397 pub fn reset_on_prompt_property() -> StyledProperty<bool> {
399 FerroProperty::register::<MaskedTextBox, _>("ResetOnPrompt", true)
400 }
401 );
402
403 ferro_property!(
404 pub fn reset_on_space_property() -> StyledProperty<bool> {
406 FerroProperty::register::<MaskedTextBox, _>("ResetOnSpace", true)
407 }
408 );
409} }
410
411impl MaskedTextBox {
412 fn static_constructor() {
413 TextBox::password_char_property().override_metadata::<MaskedTextBox>(
414 StyledPropertyMetadata::new(Some('\0')).with_coerce(Self::coerce_password_char),
415 );
416 }
417
418 fn coerce_password_char(sender: &FerroObject, base_value: char) -> char {
419 if !MaskedTextProvider::is_valid_password_char(base_value) {
420 panic!("'{base_value}' is not a valid value for PasswordChar.");
421 }
422 let textbox = sender.downcast_ref::<MaskedTextBox>().expect("the sender is not a masked text box");
423 if let Some(mask_provider) = textbox.mask_provider() {
424 if base_value == mask_provider.borrow().prompt_char() {
425 panic!("PasswordChar and PromptChar values cannot be the same.");
427 }
428 }
429
430 base_value
431 }
432
433 fn coerce_prompt_char(sender: &FerroObject, base_value: char) -> char {
434 if !MaskedTextProvider::is_valid_input_char(base_value) {
435 panic!("'{base_value}' is not a valid value for PromptChar.");
436 }
437 if base_value == sender.get_value(TextBox::password_char_property()) {
438 panic!("PasswordChar and PromptChar values cannot be the same.");
439 }
440
441 base_value
442 }
443
444 pub fn construct() -> Self {
446 Self { base: TextBox::construct(), mask_provider: RefCell::new(None), ignore_text_changes: Cell::new(false) }
447 }
448
449 pub fn new() -> Ref<Self> {
451 instantiate(Self::construct())
452 }
453
454 pub fn with_provider(masked_text_provider: &MaskedTextProvider) -> Ref<Self> {
457 let this = Self::new();
458 this.set_ascii_only(masked_text_provider.ascii_only());
460 this.set_culture(Some(masked_text_provider.culture().clone()));
461 this.set_mask(Some(masked_text_provider.mask()));
462 this.set_password_char(masked_text_provider.password_char());
463 this.set_prompt_char(masked_text_provider.prompt_char());
464 this
465 }
466
467 pub fn ascii_only(&self) -> bool {
470 self.get_value(Self::ascii_only_property())
471 }
472
473 pub fn set_ascii_only(&self, value: bool) {
474 self.set_value(Self::ascii_only_property(), value)
475 }
476
477 pub fn culture(&self) -> Option<CultureInfo> {
479 self.get_value(Self::culture_property())
480 }
481
482 pub fn set_culture(&self, value: Option<CultureInfo>) {
483 self.set_value(Self::culture_property(), value)
484 }
485
486 pub fn hide_prompt_on_leave(&self) -> bool {
489 self.get_value(Self::hide_prompt_on_leave_property())
490 }
491
492 pub fn set_hide_prompt_on_leave(&self, value: bool) {
493 self.set_value(Self::hide_prompt_on_leave_property(), value)
494 }
495
496 pub fn mask(&self) -> Option<String> {
498 self.get_value(Self::mask_property())
499 }
500
501 pub fn set_mask(&self, value: Option<&str>) {
502 self.set_value(Self::mask_property(), value.map(str::to_owned))
503 }
504
505 pub fn mask_completed(&self) -> Option<bool> {
508 self.mask_provider().map(|provider| provider.borrow().mask_completed())
509 }
510
511 pub fn mask_full(&self) -> Option<bool> {
514 self.mask_provider().map(|provider| provider.borrow().mask_full())
515 }
516
517 pub fn mask_provider(&self) -> Option<Rc<RefCell<MaskedTextProvider>>> {
519 self.mask_provider.borrow().clone()
520 }
521
522 pub fn prompt_char(&self) -> char {
525 self.get_value(Self::prompt_char_property())
526 }
527
528 pub fn set_prompt_char(&self, value: char) {
529 self.set_value(Self::prompt_char_property(), value)
530 }
531
532 pub fn reset_on_prompt(&self) -> bool {
535 self.get_value(Self::reset_on_prompt_property())
536 }
537
538 pub fn set_reset_on_prompt(&self, value: bool) {
539 self.set_value(Self::reset_on_prompt_property(), value)
540 }
541
542 pub fn reset_on_space(&self) -> bool {
545 self.get_value(Self::reset_on_space_property())
546 }
547
548 pub fn set_reset_on_space(&self, value: bool) {
549 self.set_value(Self::reset_on_space_property(), value)
550 }
551
552 fn text_length(&self) -> Option<i32> {
554 self.text().map(|text| text.encode_utf16().count() as i32)
555 }
556
557 fn get_next_character_position(&self, start_position: i32) -> i32 {
558 if let Some(provider) = self.mask_provider() {
559 let position = provider.borrow().find_edit_position_from(start_position, true);
560 if self.caret_index() != -1 {
561 return position;
562 }
563 }
564 start_position
565 }
566
567 fn refresh_text(&self, provider: Option<&Rc<RefCell<MaskedTextProvider>>>, position: i32, is_edit: bool) {
568 if let Some(provider) = provider {
569 let text = provider.borrow().to_display_string();
570 if is_edit {
571 self.set_text_from_edit(Some(text));
572 } else {
573 self.set_text_from_internal_synchronization(Some(text));
574 }
575 self.set_current_value(TextBox::caret_index_property(), position);
576 }
577 }
578}