fyrox_ui/
text_box.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! TextBox is a text widget that allows you to edit text and create specialized input fields. See [`TextBox`] docs for more
22//! info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::{
27    brush::Brush,
28    core::{
29        algebra::{Point2, Vector2},
30        color::Color,
31        math::Rect,
32        parking_lot::Mutex,
33        pool::Handle,
34        reflect::prelude::*,
35        type_traits::prelude::*,
36        uuid_provider,
37        variable::InheritableVariable,
38        visitor::prelude::*,
39    },
40    define_constructor,
41    draw::{CommandTexture, Draw, DrawingContext},
42    font::FontResource,
43    formatted_text::{FormattedText, FormattedTextBuilder, WrapMode},
44    message::{CursorIcon, KeyCode, MessageDirection, MouseButton, UiMessage},
45    text::TextMessage,
46    widget::{Widget, WidgetBuilder, WidgetMessage},
47    BuildContext, Control, HorizontalAlignment, UiNode, UserInterface, VerticalAlignment,
48};
49use copypasta::ClipboardProvider;
50use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
51use std::{
52    cell::RefCell,
53    fmt::{Debug, Formatter},
54    ops::{Deref, DerefMut},
55    sync::Arc,
56};
57use strum_macros::{AsRefStr, EnumString, VariantNames};
58
59/// A message that could be used to alternate text box widget's state or receive changes from it.
60///
61/// # Important notes
62///
63/// Text box widget also supports [`TextMessage`] and [`WidgetMessage`].
64#[derive(Debug, Clone, PartialEq)]
65pub enum TextBoxMessage {
66    /// Used to change selection brush of a text box. Use [TextBoxMessage::selection_brush`] to create the message.
67    SelectionBrush(Brush),
68    /// Used to change caret brush of a text box. Use [TextBoxMessage::caret_brush`] to create the message.
69    CaretBrush(Brush),
70    /// Used to change text commit mode of a text box. Use [TextBoxMessage::text_commit_mode`] to create the message.
71    TextCommitMode(TextCommitMode),
72    /// Used to enable or disable multiline mode of a text box. Use [TextBoxMessage::multiline`] to create the message.
73    Multiline(bool),
74    /// Used to enable or disable an ability to edit text box content. Use [TextBoxMessage::editable`] to create the message.
75    Editable(bool),
76}
77
78impl TextBoxMessage {
79    define_constructor!(
80        /// Creates [`TextBoxMessage::SelectionBrush`].
81        TextBoxMessage:SelectionBrush => fn selection_brush(Brush), layout: false
82    );
83    define_constructor!(
84        /// Creates [`TextBoxMessage::CaretBrush`].
85        TextBoxMessage:CaretBrush => fn caret_brush(Brush), layout: false
86    );
87    define_constructor!(
88        /// Creates [`TextBoxMessage::TextCommitMode`].
89        TextBoxMessage:TextCommitMode => fn text_commit_mode(TextCommitMode), layout: false
90    );
91    define_constructor!(
92        /// Creates [`TextBoxMessage::Multiline`].
93        TextBoxMessage:Multiline => fn multiline(bool), layout: false
94    );
95    define_constructor!(
96        /// Creates [`TextBoxMessage::Editable`].
97        TextBoxMessage:Editable => fn editable(bool), layout: false
98    );
99}
100
101/// Specifies a direction on horizontal axis.
102#[derive(Copy, Clone, PartialEq, Eq)]
103pub enum HorizontalDirection {
104    /// Left direction.
105    Left,
106    /// Right direction.
107    Right,
108}
109
110/// Specifies a direction on vertical axis.
111#[derive(Copy, Clone, PartialEq, Eq)]
112pub enum VerticalDirection {
113    /// Down direction.
114    Down,
115    /// Up direction.
116    Up,
117}
118
119pub use crate::formatted_text::Position;
120use crate::style::resource::StyleResourceExt;
121use crate::style::{Style, StyledProperty};
122
123/// Defines the way, how the text box widget will commit the text that was typed in
124#[derive(
125    Copy,
126    Clone,
127    PartialOrd,
128    PartialEq,
129    Eq,
130    Ord,
131    Hash,
132    Debug,
133    Default,
134    Visit,
135    Reflect,
136    AsRefStr,
137    EnumString,
138    VariantNames,
139    TypeUuidProvider,
140)]
141#[repr(u32)]
142#[type_uuid(id = "5fb7d6f0-c151-4a30-8350-2060749d74c6")]
143pub enum TextCommitMode {
144    /// Text box will immediately send [`TextMessage::Text`] message after any change (after any pressed button).
145    Immediate = 0,
146
147    /// Text box will send Text message only when it loses focus (when a user "clicks" outside of it or with any other
148    /// event that forces the text box to lose focus).
149    LostFocus = 1,
150
151    /// Text box will send Text message when it loses focus or if Enter key was pressed. This is **default** behavior.
152    ///
153    /// # Notes
154    ///
155    /// In case of multiline text box hitting Enter key won't commit the text!
156    #[default]
157    LostFocusPlusEnter = 2,
158
159    /// Text box will send Text message when it loses focus or if Enter key was pressed, but **only** if the content
160    /// of the text box changed since the last time it gained focus or the text was committed.
161    ///
162    /// # Notes
163    ///
164    /// In case of multiline text box hitting Enter key won't commit the text!
165    Changed = 3,
166}
167
168/// Defines a set of two positions in the text, that forms a specific range.
169#[derive(Copy, Clone, PartialEq, Eq, Debug, Visit, Reflect, Default, TypeUuidProvider)]
170#[type_uuid(id = "04c8101b-cb34-47a5-af34-ecfb9b2fc426")]
171pub struct SelectionRange {
172    /// Position of the beginning.
173    pub begin: Position,
174    /// Position of the end.
175    pub end: Position,
176}
177
178impl SelectionRange {
179    /// Creates a new range, that have its begin always before end. It could be useful in case if user
180    /// selects a range right-to-left.
181    #[must_use = "method creates new value which must be used"]
182    pub fn normalized(&self) -> SelectionRange {
183        SelectionRange {
184            begin: self.left(),
185            end: self.right(),
186        }
187    }
188    /// Creates a Range iterator of the positions in this range from left to right, excluding the rightmost position.
189    pub fn range(&self) -> std::ops::Range<Position> {
190        std::ops::Range {
191            start: self.left(),
192            end: self.right(),
193        }
194    }
195    /// `true` if the given position is inside this range, including the beginning and end.
196    pub fn contains(&self, position: Position) -> bool {
197        (self.begin..=self.end).contains(&position)
198    }
199    /// The leftmost position.
200    pub fn left(&self) -> Position {
201        Position::min(self.begin, self.end)
202    }
203    /// The rightmost position.
204    pub fn right(&self) -> Position {
205        Position::max(self.begin, self.end)
206    }
207}
208
209/// Defines a function, that could be used to filter out desired characters. It must return `true` for characters, that pass
210/// the filter, and `false` - otherwise.
211pub type FilterCallback = dyn FnMut(char) -> bool + Send;
212
213/// TextBox is a text widget that allows you to edit text and create specialized input fields. It has various options like
214/// word wrapping, text alignment, and so on.
215///
216/// ## How to create
217///
218/// An instance of the TextBox widget could be created like so:
219///
220/// ```rust,no_run
221/// # use fyrox_ui::{
222/// #     core::pool::Handle,
223/// #     text_box::TextBoxBuilder, widget::WidgetBuilder, UiNode, UserInterface
224/// # };
225/// fn create_text_box(ui: &mut UserInterface, text: &str) -> Handle<UiNode> {
226///     TextBoxBuilder::new(WidgetBuilder::new())
227///         .with_text(text)
228///         .build(&mut ui.build_ctx())
229/// }
230/// ```
231///
232/// ## Text alignment and word wrapping
233///
234/// There are various text alignment options for both vertical and horizontal axes. Typical alignment values are:
235/// [`HorizontalAlignment::Left`], [`HorizontalAlignment::Center`], [`HorizontalAlignment::Right`] for horizontal axis,
236/// and [`VerticalAlignment::Top`], [`VerticalAlignment::Center`], [`VerticalAlignment::Bottom`] for vertical axis.
237/// An instance of centered text could be created like so:
238///
239/// ```rust,no_run
240/// # use fyrox_ui::{
241/// #     core::pool::Handle,
242/// #     text_box::TextBoxBuilder, widget::WidgetBuilder, HorizontalAlignment, UiNode, UserInterface,
243/// #     VerticalAlignment,
244/// # };
245/// fn create_centered_text(ui: &mut UserInterface, text: &str) -> Handle<UiNode> {
246///     TextBoxBuilder::new(WidgetBuilder::new())
247///         .with_horizontal_text_alignment(HorizontalAlignment::Center)
248///         .with_vertical_text_alignment(VerticalAlignment::Center)
249///     .with_text(text)
250///     .build(&mut ui.build_ctx())
251/// }
252/// ```
253///
254/// Long text is usually needs to wrap on available bounds, there are three possible options for word wrapping:
255/// [`WrapMode::NoWrap`], [`WrapMode::Letter`], [`WrapMode::Word`]. An instance of text with word-based wrapping could be
256/// created like so:
257///
258/// ```rust,no_run
259/// # use fyrox_ui::{
260/// #     core::pool::Handle,
261/// #     formatted_text::WrapMode, text_box::TextBoxBuilder, widget::WidgetBuilder, UiNode,
262/// #     UserInterface,
263/// # };
264/// fn create_text_with_word_wrap(ui: &mut UserInterface, text: &str) -> Handle<UiNode> {
265///     TextBoxBuilder::new(WidgetBuilder::new())
266///         .with_wrap(WrapMode::Word)
267///         .with_text(text)
268///         .build(&mut ui.build_ctx())
269/// }
270/// ```
271///
272/// ## Fonts and colors
273///
274/// To set a color of the text just use [`WidgetBuilder::with_foreground`] while building the text instance:
275///
276/// ```rust,no_run
277/// # use fyrox_ui::{
278/// #     core::{color::Color, pool::Handle},
279/// #     brush::Brush, text_box::TextBoxBuilder, widget::WidgetBuilder, UiNode, UserInterface
280/// # };
281/// fn create_text(ui: &mut UserInterface, text: &str) -> Handle<UiNode> {
282///     //                  vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
283///     TextBoxBuilder::new(WidgetBuilder::new().with_foreground(Brush::Solid(Color::RED).into()))
284///         .with_text(text)
285///         .build(&mut ui.build_ctx())
286/// }
287/// ```
288///
289/// By default, text is created with default font, however it is possible to set any custom font:
290///
291/// ```rust,no_run
292/// # use fyrox_resource::manager::ResourceManager;
293/// # use fyrox_ui::{
294/// #     core::{futures::executor::block_on, pool::Handle},
295/// #     text_box::TextBoxBuilder,
296/// #     font::{Font},
297/// #     widget::WidgetBuilder,
298/// #     UiNode, UserInterface,
299/// # };
300///
301/// fn create_text(ui: &mut UserInterface, resource_manager: &ResourceManager, text: &str) -> Handle<UiNode> {
302///     TextBoxBuilder::new(WidgetBuilder::new())
303///         .with_font(resource_manager.request::<Font>("path/to/your/font.ttf"))
304///         .with_text(text)
305///         .with_font_size(20.0f32.into())
306///         .build(&mut ui.build_ctx())
307/// }
308/// ```
309///
310/// Please refer to [`FontResource`] to learn more about fonts.
311///
312/// ### Font size
313///
314/// Use [`TextBoxBuilder::with_font_size`] or send [`TextMessage::font_size`] to your TextBox widget instance
315/// to set the font size of it.
316///
317/// ## Messages
318///
319/// TextBox widget accepts the following list of messages:
320///
321/// - [`TextBoxMessage::SelectionBrush`] - change the brush that is used to highlight selection.
322/// - [`TextBoxMessage::CaretBrush`] - changes the brush of the caret (small blinking vertical line).
323/// - [`TextBoxMessage::TextCommitMode`] - changes the [text commit mode](TextBox#text-commit-mode).
324/// - [`TextBoxMessage::Multiline`] - makes the TextBox either multiline (`true`) or single line (`false`)
325/// - [`TextBoxMessage::Editable`] - enables or disables editing of the text.
326///
327/// **Important:** Please keep in mind, that TextBox widget also accepts [`TextMessage`]s. An example of changing text at
328/// runtime could be something like this:
329///
330/// ```rust,no_run
331/// # use fyrox_ui::{
332/// #     core::pool::Handle,
333/// #     message::{MessageDirection},
334/// #     UiNode, UserInterface,
335/// #     text::TextMessage
336/// # };
337/// fn request_change_text(ui: &UserInterface, text_box_widget_handle: Handle<UiNode>, text: &str) {
338///     ui.send_message(TextMessage::text(
339///         text_box_widget_handle,
340///         MessageDirection::ToWidget,
341///         text.to_owned(),
342///     ))
343/// }
344/// ```
345///
346/// Please keep in mind, that like any other situation when you "changing" something via messages, you should remember
347/// that the change is **not** immediate. The change will be applied on `ui.poll_message(..)` call somewhere in your
348/// code.
349///
350/// ## Shortcuts
351///
352/// There are number of default shortcuts that can be used to speed up text editing:
353///
354/// - `Ctrl+A` - select all
355/// - `Ctrl+C` - copy selected text
356/// - `Ctrl+V` - paste text from clipboard
357/// - `Ctrl+Home` - move caret to the beginning of the text
358/// - `Ctrl+End` - move caret to the beginning of the text
359/// - `Shift+Home` - select everything from current caret position until the beginning of current line
360/// - `Shift+End` - select everything from current caret position until the end of current line
361/// - `Arrows` - move caret accordingly
362/// - `Delete` - deletes next character
363/// - `Backspace` - deletes previous character
364/// - `Enter` - new line (if multiline mode is set) or `commit` message
365///
366/// ## Multiline Text Box
367///
368/// By default, text box will not add new line character to the text if you press `Enter` on keyboard. To enable this
369/// functionality use [`TextBoxBuilder::with_multiline`]
370///
371/// ## Read-only Mode
372///
373/// You can enable or disable content editing by using read-only mode. Use [`TextBoxBuilder::with_editable`] at build stage.
374///
375/// ## Mask Character
376///
377/// You can specify replacement character for every other characters, this is useful option for password fields. Use
378/// [`TextBoxBuilder::with_mask_char`] at build stage. For example, you can set replacement character to asterisk `*` using
379/// `.with_mask_char(Some('*'))`
380///
381/// ## Text Commit Mode
382///
383/// In many situations you don't need the text box to send `new text` message every new character, you either want this
384/// message if `Enter` key is pressed or TextBox has lost keyboard focus (or both). There is [`TextBoxBuilder::with_text_commit_mode`]
385/// on builder specifically for that purpose. Use one of the following modes:
386///
387/// - [`TextCommitMode::Immediate`] - text box will immediately send [`TextMessage::Text`] message after any change.
388/// - [`TextCommitMode::LostFocus`] - text box will send [`TextMessage::Text`] message only when it loses focus.
389/// - [`TextCommitMode::LostFocusPlusEnter`] - text box will send [`TextMessage::Text`] message when it loses focus or if Enter
390/// key was pressed. This is **default** behavior. In case of multiline text box hitting Enter key won't commit text!
391///
392/// ## Filtering
393///
394/// It is possible specify custom input filter, it can be useful if you're creating special input fields like numerical or
395/// phone number. A filter can be specified at build stage like so:
396///
397/// ```rust,no_run
398/// # use fyrox_ui::{
399/// #     core::pool::Handle,
400/// #     text_box::TextBoxBuilder, widget::WidgetBuilder, UiNode, UserInterface
401/// # };
402/// # use std::sync::Arc;
403/// # use fyrox_core::parking_lot::Mutex;
404/// fn create_text_box(ui: &mut UserInterface) -> Handle<UiNode> {
405///     TextBoxBuilder::new(WidgetBuilder::new())
406///         // Specify a filter that will pass only digits.
407///         .with_filter(Arc::new(Mutex::new(|c: char| c.is_ascii_digit())))
408///         .build(&mut ui.build_ctx())
409/// }
410/// ```
411///
412/// ## Style
413///
414/// You can change brush of caret by using [`TextBoxBuilder::with_caret_brush`] and also selection brush by using
415/// [`TextBoxBuilder::with_selection_brush`], it could be useful if you don't like default colors.
416#[derive(Default, Clone, Visit, Reflect, ComponentProvider)]
417pub struct TextBox {
418    /// Base widget of the text box.
419    pub widget: Widget,
420    /// Current position of the caret in the text box.
421    pub caret_position: InheritableVariable<Position>,
422    /// Whether the caret is visible or not.
423    pub caret_visible: InheritableVariable<bool>,
424    /// Internal blinking timer.
425    pub blink_timer: InheritableVariable<f32>,
426    /// Blinking interval in seconds.
427    pub blink_interval: InheritableVariable<f32>,
428    /// Formatted text that stores actual text and performs its layout. See [`FormattedText`] docs for more info.
429    pub formatted_text: RefCell<FormattedText>,
430    /// Current selection range.
431    pub selection_range: InheritableVariable<Option<SelectionRange>>,
432    /// `true` if the text box is in selection mode.
433    pub selecting: bool,
434    /// Stores the location of the caret before it was moved by mouse click.
435    #[visit(skip)]
436    pub before_click_position: Position,
437    /// `true` if the text box is focused.
438    pub has_focus: bool,
439    /// Current caret brush of the text box.
440    pub caret_brush: InheritableVariable<Brush>,
441    /// Current selection brush of the text box.
442    pub selection_brush: InheritableVariable<Brush>,
443    /// Current character filter of the text box.
444    #[visit(skip)]
445    #[reflect(hidden)]
446    pub filter: Option<Arc<Mutex<FilterCallback>>>,
447    /// Current text commit mode of the text box.
448    pub commit_mode: InheritableVariable<TextCommitMode>,
449    /// `true` if the the multiline mode is active.
450    pub multiline: InheritableVariable<bool>,
451    /// `true` if the text box is editable.
452    pub editable: InheritableVariable<bool>,
453    /// Position of the local "camera" (viewing rectangle) of the text box.
454    pub view_position: InheritableVariable<Vector2<f32>>,
455    /// A list of custom characters that will be treated as whitespace.
456    pub skip_chars: InheritableVariable<Vec<char>>,
457    /// Stored copy of most recent commit, when `commit_mode` is [TextCommitMode::Changed].
458    #[visit(skip)]
459    #[reflect(hidden)]
460    pub recent: Vec<char>,
461}
462
463impl ConstructorProvider<UiNode, UserInterface> for TextBox {
464    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
465        GraphNodeConstructor::new::<Self>()
466            .with_variant("Text Box", |ui| {
467                TextBoxBuilder::new(WidgetBuilder::new().with_name("Text Box"))
468                    .with_text("Text")
469                    .build(&mut ui.build_ctx())
470                    .into()
471            })
472            .with_group("Input")
473    }
474}
475
476impl Debug for TextBox {
477    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
478        f.write_str("TextBox")
479    }
480}
481
482crate::define_widget_deref!(TextBox);
483
484impl TextBox {
485    fn commit_if_changed(&mut self, ui: &mut UserInterface) {
486        let formatted_text = self.formatted_text.borrow();
487        let raw = formatted_text.get_raw_text();
488        if self.recent != raw {
489            self.recent.clear();
490            self.recent.extend(raw);
491            ui.send_message(TextMessage::text(
492                self.handle,
493                MessageDirection::FromWidget,
494                formatted_text.text(),
495            ));
496        }
497    }
498    fn filter_paste_str_multiline(&self, str: &str) -> String {
499        let mut str = str.replace("\r\n", "\n");
500        str.retain(|c| c == '\n' || !c.is_control());
501        if let Some(filter) = self.filter.as_ref() {
502            let filter = &mut *filter.lock();
503            str.retain(filter);
504        }
505        str
506    }
507
508    fn filter_paste_str_single_line(&self, str: &str) -> String {
509        let mut str: String = str
510            .chars()
511            .map(|c| if c == '\n' { ' ' } else { c })
512            .filter(|c| !c.is_control())
513            .collect();
514        if let Some(filter) = self.filter.as_ref() {
515            let filter = &mut *filter.lock();
516            str.retain(filter);
517        }
518        str
519    }
520
521    fn reset_blink(&mut self) {
522        self.caret_visible.set_value_and_mark_modified(true);
523        self.blink_timer.set_value_and_mark_modified(0.0);
524    }
525
526    fn move_caret(&mut self, position: Position, select: bool) {
527        let text = self.formatted_text.borrow();
528        let lines = text.get_lines();
529        if select && !lines.is_empty() {
530            if self.selection_range.is_none() {
531                self.selection_range
532                    .set_value_and_mark_modified(Some(SelectionRange {
533                        begin: *self.caret_position,
534                        end: *self.caret_position,
535                    }));
536            }
537        } else {
538            self.selection_range.set_value_and_mark_modified(None);
539        }
540
541        if lines.is_empty() {
542            drop(text);
543            self.set_caret_position(Default::default());
544            return;
545        }
546
547        drop(text);
548
549        if let Some(selection_range) = self.selection_range.as_mut() {
550            if select {
551                if *self.caret_position != selection_range.end {
552                    if !selection_range.contains(position) {
553                        if position < selection_range.left() {
554                            selection_range.begin = selection_range.right();
555                        } else {
556                            selection_range.begin = selection_range.left();
557                        }
558                        selection_range.end = position;
559                    }
560                } else {
561                    selection_range.end = position;
562                }
563            }
564        }
565
566        self.set_caret_position(position);
567        self.ensure_caret_visible();
568    }
569
570    fn move_caret_x(&mut self, offset: isize, select: bool) {
571        let pos = self
572            .formatted_text
573            .borrow()
574            .get_relative_position_x(*self.caret_position, offset);
575        self.move_caret(pos, select);
576    }
577
578    fn move_caret_y(&mut self, offset: isize, select: bool) {
579        let pos = self
580            .formatted_text
581            .borrow()
582            .get_relative_position_y(*self.caret_position, offset);
583        self.move_caret(pos, select);
584    }
585
586    /// Maps input [`Position`] to a linear position in character array.
587    /// The index returned is the index of the character after the position, which may be
588    /// out-of-bounds if thee position is at the end of the text.
589    /// You should check the index before trying to use it to fetch data from inner array of characters.
590    pub fn position_to_char_index_unclamped(&self, position: Position) -> Option<usize> {
591        self.formatted_text
592            .borrow()
593            .position_to_char_index_unclamped(position)
594    }
595
596    /// Maps input [`Position`] to a linear position in character array.
597    /// The index returned is usually the index of the character after the position,
598    /// but if the position is at the end of a line then return the index of the character _before_ the position.
599    /// In other words, the last two positions of each line are mapped to the same character index.
600    /// Output index will always be valid for fetching, if the method returned `Some(index)`.
601    /// The index however cannot be used for text insertion, because it cannot point to a "place after last char".
602    pub fn position_to_char_index_clamped(&self, position: Position) -> Option<usize> {
603        self.formatted_text
604            .borrow()
605            .position_to_char_index_clamped(position)
606    }
607
608    /// Maps linear character index (as in string) to its actual location in the text.
609    pub fn char_index_to_position(&self, i: usize) -> Option<Position> {
610        self.formatted_text.borrow().char_index_to_position(i)
611    }
612
613    /// Returns end position of the text.
614    pub fn end_position(&self) -> Position {
615        self.formatted_text.borrow().end_position()
616    }
617
618    /// Returns a position of a next word after the caret in the text.
619    pub fn find_next_word(&self, from: Position) -> Position {
620        self.position_to_char_index_unclamped(from)
621            .and_then(|i| {
622                self.formatted_text
623                    .borrow()
624                    .get_raw_text()
625                    .iter()
626                    .enumerate()
627                    .skip(i)
628                    .skip_while(|(_, c)| !(c.is_whitespace() || self.skip_chars.contains(*c)))
629                    .find(|(_, c)| !(c.is_whitespace() || self.skip_chars.contains(*c)))
630                    .and_then(|(n, _)| self.char_index_to_position(n))
631            })
632            .unwrap_or_else(|| self.end_position())
633    }
634
635    /// Returns a position of a next word before the caret in the text.
636    pub fn find_prev_word(&self, from: Position) -> Position {
637        self.position_to_char_index_unclamped(from)
638            .and_then(|i| {
639                let text = self.formatted_text.borrow();
640                let len = text.get_raw_text().len();
641                text.get_raw_text()
642                    .iter()
643                    .enumerate()
644                    .rev()
645                    .skip(len.saturating_sub(i))
646                    .skip_while(|(_, c)| !(c.is_whitespace() || self.skip_chars.contains(*c)))
647                    .find(|(_, c)| !(c.is_whitespace() || self.skip_chars.contains(*c)))
648                    .and_then(|(n, _)| self.char_index_to_position(n + 1))
649            })
650            .unwrap_or_default()
651    }
652
653    /// Inserts given character at current caret position.
654    fn insert_char(&mut self, c: char, ui: &UserInterface) {
655        self.remove_before_insert();
656        let position = self
657            .position_to_char_index_unclamped(*self.caret_position)
658            .unwrap_or_default();
659        self.formatted_text
660            .borrow_mut()
661            .insert_char(c, position)
662            .build();
663        self.set_caret_position(
664            self.char_index_to_position(position + 1)
665                .unwrap_or_default(),
666        );
667        if *self.commit_mode == TextCommitMode::Immediate {
668            ui.send_message(TextMessage::text(
669                self.handle,
670                MessageDirection::FromWidget,
671                self.formatted_text.borrow().text(),
672            ));
673        }
674    }
675
676    fn insert_str(&mut self, str: &str, ui: &UserInterface) {
677        if str.is_empty() {
678            return;
679        }
680        let str: String = if *self.multiline {
681            self.filter_paste_str_multiline(str)
682        } else {
683            self.filter_paste_str_single_line(str)
684        };
685        self.remove_before_insert();
686        let position = self
687            .position_to_char_index_unclamped(*self.caret_position)
688            .unwrap_or_default();
689        let mut text = self.formatted_text.borrow_mut();
690        text.insert_str(&str, position);
691        text.build();
692        drop(text);
693        self.set_caret_position(
694            self.char_index_to_position(position + str.chars().count())
695                .unwrap_or_default(),
696        );
697        if *self.commit_mode == TextCommitMode::Immediate {
698            ui.send_message(TextMessage::text(
699                self.handle,
700                MessageDirection::FromWidget,
701                self.formatted_text.borrow().text(),
702            ));
703        }
704    }
705
706    fn remove_before_insert(&mut self) {
707        let Some(selection) = *self.selection_range else {
708            return;
709        };
710        let range = self
711            .formatted_text
712            .borrow()
713            .position_range_to_char_index_range(selection.range());
714        if range.is_empty() {
715            return;
716        }
717        self.formatted_text.borrow_mut().remove_range(range);
718        self.selection_range.set_value_and_mark_modified(None);
719        self.set_caret_position(selection.left());
720    }
721
722    /// Returns current text length in characters.
723    pub fn get_text_len(&self) -> usize {
724        self.formatted_text.borrow_mut().get_raw_text().len()
725    }
726
727    /// Returns current position the caret in the local coordinates.
728    pub fn caret_local_position(&self) -> Vector2<f32> {
729        self.formatted_text
730            .borrow_mut()
731            .position_to_local(*self.caret_position)
732    }
733
734    fn point_to_view_pos(&self, position: Vector2<f32>) -> Vector2<f32> {
735        position - *self.view_position
736    }
737
738    fn rect_to_view_pos(&self, mut rect: Rect<f32>) -> Rect<f32> {
739        rect.position -= *self.view_position;
740        rect
741    }
742
743    fn ensure_caret_visible(&mut self) {
744        let local_bounds = self.bounding_rect();
745        let caret_view_position = self.point_to_view_pos(self.caret_local_position());
746        // Move view position to contain the caret + add some spacing.
747        let spacing_step = self
748            .formatted_text
749            .borrow()
750            .get_font()
751            .state()
752            .data()
753            .map(|font| font.ascender(*self.height))
754            .unwrap_or_default();
755        let spacing = spacing_step * 3.0;
756        let top_left_corner = local_bounds.left_top_corner();
757        let bottom_right_corner = local_bounds.right_bottom_corner();
758        if caret_view_position.x > bottom_right_corner.x {
759            self.view_position.x += caret_view_position.x - bottom_right_corner.x + spacing;
760        }
761        if caret_view_position.x < top_left_corner.x {
762            self.view_position.x -= top_left_corner.x - caret_view_position.x + spacing;
763        }
764        if caret_view_position.y > bottom_right_corner.y {
765            self.view_position.y += bottom_right_corner.y - caret_view_position.y + spacing;
766        }
767        if caret_view_position.y < top_left_corner.y {
768            self.view_position.y -= top_left_corner.y - caret_view_position.y + spacing;
769        }
770        self.view_position.x = self.view_position.x.max(0.0);
771        self.view_position.y = self.view_position.y.max(0.0);
772    }
773
774    fn remove_char(&mut self, direction: HorizontalDirection, ui: &UserInterface) {
775        if let Some(selection) = *self.selection_range {
776            self.remove_range(ui, selection);
777            return;
778        }
779        let Some(position) = self.position_to_char_index_unclamped(*self.caret_position) else {
780            return;
781        };
782
783        let text_len = self.get_text_len();
784        if text_len != 0 {
785            let position = match direction {
786                HorizontalDirection::Left => {
787                    if position == 0 {
788                        return;
789                    }
790                    position - 1
791                }
792                HorizontalDirection::Right => {
793                    if position >= text_len {
794                        return;
795                    }
796                    position
797                }
798            };
799
800            let mut text = self.formatted_text.borrow_mut();
801            text.remove_at(position);
802            text.build();
803            drop(text);
804
805            if *self.commit_mode == TextCommitMode::Immediate {
806                ui.send_message(TextMessage::text(
807                    self.handle(),
808                    MessageDirection::FromWidget,
809                    self.formatted_text.borrow().text(),
810                ));
811            }
812
813            self.set_caret_position(self.char_index_to_position(position).unwrap_or_default());
814        }
815    }
816
817    fn remove_range(&mut self, ui: &UserInterface, selection: SelectionRange) {
818        let range = self
819            .formatted_text
820            .borrow()
821            .position_range_to_char_index_range(selection.range());
822        if range.is_empty() {
823            return;
824        }
825        self.formatted_text.borrow_mut().remove_range(range);
826        self.formatted_text.borrow_mut().build();
827        self.set_caret_position(selection.left());
828        self.selection_range.set_value_and_mark_modified(None);
829        if *self.commit_mode == TextCommitMode::Immediate {
830            ui.send_message(TextMessage::text(
831                self.handle(),
832                MessageDirection::FromWidget,
833                self.formatted_text.borrow().text(),
834            ));
835        }
836    }
837
838    /// Checks whether the input position is correct (in bounds) or not.
839    pub fn is_valid_position(&self, position: Position) -> bool {
840        self.formatted_text
841            .borrow()
842            .get_lines()
843            .get(position.line)
844            .is_some_and(|line| position.offset < line.len())
845    }
846
847    fn set_caret_position(&mut self, position: Position) {
848        self.caret_position.set_value_and_mark_modified(
849            self.formatted_text
850                .borrow()
851                .nearest_valid_position(position),
852        );
853        self.ensure_caret_visible();
854        self.reset_blink();
855    }
856
857    /// Tries to map screen space position to a position in the text.
858    pub fn screen_pos_to_text_pos(&self, screen_point: Vector2<f32>) -> Option<Position> {
859        // Transform given point into local space of the text box - this way calculations can be done
860        // as usual, without a need for special math.
861        let point_to_check = self
862            .visual_transform
863            .try_inverse()?
864            .transform_point(&Point2::from(screen_point))
865            .coords;
866
867        Some(
868            self.formatted_text
869                .borrow_mut()
870                .local_to_position(point_to_check),
871        )
872    }
873
874    /// Returns current text of text box.
875    pub fn text(&self) -> String {
876        self.formatted_text.borrow().text()
877    }
878
879    /// Returns current word wrapping mode of text box.
880    pub fn wrap_mode(&self) -> WrapMode {
881        self.formatted_text.borrow().wrap_mode()
882    }
883
884    /// Returns current font of text box.
885    pub fn font(&self) -> FontResource {
886        self.formatted_text.borrow().get_font()
887    }
888
889    /// Returns current vertical alignment of text box.
890    pub fn vertical_alignment(&self) -> VerticalAlignment {
891        self.formatted_text.borrow().vertical_alignment()
892    }
893
894    /// Returns current horizontal alignment of text box.
895    pub fn horizontal_alignment(&self) -> HorizontalAlignment {
896        self.formatted_text.borrow().horizontal_alignment()
897    }
898
899    fn select_word(&mut self, position: Position) {
900        if let Some(index) = self.position_to_char_index_clamped(position) {
901            let text_ref = self.formatted_text.borrow();
902            let text = text_ref.get_raw_text();
903            let search_whitespace = !text[index].is_whitespace();
904
905            let mut left_index = index;
906            while left_index > 0 {
907                let is_whitespace = text[left_index].is_whitespace();
908                if search_whitespace && is_whitespace || !search_whitespace && !is_whitespace {
909                    left_index += 1;
910                    break;
911                }
912                left_index = left_index.saturating_sub(1);
913            }
914
915            let mut right_index = index;
916            while right_index < text.len() {
917                let is_whitespace = text[right_index].is_whitespace();
918                if search_whitespace && is_whitespace || !search_whitespace && !is_whitespace {
919                    break;
920                }
921
922                right_index += 1;
923            }
924
925            drop(text_ref);
926
927            if let (Some(left), Some(right)) = (
928                self.char_index_to_position(left_index),
929                self.char_index_to_position(right_index),
930            ) {
931                self.selection_range
932                    .set_value_and_mark_modified(Some(SelectionRange {
933                        begin: left,
934                        end: right,
935                    }));
936            }
937        }
938    }
939}
940
941uuid_provider!(TextBox = "536276f2-a175-4c05-a376-5a7d8bf0d10b");
942
943impl Control for TextBox {
944    fn measure_override(&self, _: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
945        self.formatted_text
946            .borrow_mut()
947            .set_super_sampling_scale(self.visual_max_scaling())
948            .set_constraint(available_size)
949            .build()
950    }
951
952    fn on_visual_transform_changed(&self) {
953        self.formatted_text
954            .borrow_mut()
955            .set_super_sampling_scale(self.visual_max_scaling())
956            .build();
957    }
958
959    fn draw(&self, drawing_context: &mut DrawingContext) {
960        let bounds = self.widget.bounding_rect();
961        drawing_context.push_rect_filled(&bounds, None);
962        drawing_context.commit(
963            self.clip_bounds(),
964            self.widget.background(),
965            CommandTexture::None,
966            None,
967        );
968
969        self.formatted_text
970            .borrow_mut()
971            .set_brush(self.widget.foreground());
972
973        let view_bounds = self.rect_to_view_pos(bounds);
974        if let Some(ref selection_range) = self.selection_range.map(|r| r.normalized()) {
975            let text = self.formatted_text.borrow();
976            let lines = text.get_lines();
977            if selection_range.begin.line == selection_range.end.line {
978                if let Some(line) = lines.get(selection_range.begin.line) {
979                    // Begin line
980                    let offset = text
981                        .get_range_width(line.begin..(line.begin + selection_range.begin.offset));
982                    let width = text.get_range_width(
983                        (line.begin + selection_range.begin.offset)
984                            ..(line.begin + selection_range.end.offset),
985                    );
986                    let selection_bounds = Rect::new(
987                        view_bounds.x() + line.x_offset + offset,
988                        view_bounds.y() + line.y_offset,
989                        width,
990                        line.height,
991                    );
992                    drawing_context.push_rect_filled(&selection_bounds, None);
993                }
994            } else {
995                for (i, line) in text.get_lines().iter().enumerate() {
996                    if i >= selection_range.begin.line && i <= selection_range.end.line {
997                        let selection_bounds = if i == selection_range.begin.line {
998                            // Begin line
999                            let offset = text.get_range_width(
1000                                line.begin..(line.begin + selection_range.begin.offset),
1001                            );
1002                            let width = text.get_range_width(
1003                                (line.begin + selection_range.begin.offset)..line.end,
1004                            );
1005                            Rect::new(
1006                                view_bounds.x() + line.x_offset + offset,
1007                                view_bounds.y() + line.y_offset,
1008                                width,
1009                                line.height,
1010                            )
1011                        } else if i == selection_range.end.line {
1012                            // End line
1013                            let width = text.get_range_width(
1014                                line.begin..(line.begin + selection_range.end.offset),
1015                            );
1016                            Rect::new(
1017                                view_bounds.x() + line.x_offset,
1018                                view_bounds.y() + line.y_offset,
1019                                width,
1020                                line.height,
1021                            )
1022                        } else {
1023                            // Everything between
1024                            Rect::new(
1025                                view_bounds.x() + line.x_offset,
1026                                view_bounds.y() + line.y_offset,
1027                                line.width,
1028                                line.height,
1029                            )
1030                        };
1031                        drawing_context.push_rect_filled(&selection_bounds, None);
1032                    }
1033                }
1034            }
1035        }
1036        drawing_context.commit(
1037            self.clip_bounds(),
1038            (*self.selection_brush).clone(),
1039            CommandTexture::None,
1040            None,
1041        );
1042
1043        let local_position = self.point_to_view_pos(bounds.position);
1044        drawing_context.draw_text(
1045            self.clip_bounds(),
1046            local_position,
1047            &self.formatted_text.borrow(),
1048        );
1049
1050        if *self.caret_visible {
1051            let caret_pos = self.point_to_view_pos(self.caret_local_position());
1052            let caret_bounds = Rect::new(
1053                caret_pos.x,
1054                caret_pos.y,
1055                2.0,
1056                **self.formatted_text.borrow().font_size(),
1057            );
1058            drawing_context.push_rect_filled(&caret_bounds, None);
1059            drawing_context.commit(
1060                self.clip_bounds(),
1061                (*self.caret_brush).clone(),
1062                CommandTexture::None,
1063                None,
1064            );
1065        }
1066    }
1067
1068    fn update(&mut self, dt: f32, _ui: &mut UserInterface) {
1069        if self.has_focus {
1070            *self.blink_timer += dt;
1071            if *self.blink_timer >= *self.blink_interval {
1072                self.blink_timer.set_value_and_mark_modified(0.0);
1073                self.caret_visible
1074                    .set_value_and_mark_modified(!*self.caret_visible);
1075            }
1076        } else {
1077            self.caret_visible.set_value_and_mark_modified(false);
1078        }
1079    }
1080
1081    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
1082        self.widget.handle_routed_message(ui, message);
1083
1084        if message.destination() == self.handle() {
1085            if let Some(msg) = message.data::<WidgetMessage>() {
1086                match msg {
1087                    WidgetMessage::Text(text)
1088                        if !ui.keyboard_modifiers().control
1089                            && !ui.keyboard_modifiers().alt
1090                            && *self.editable =>
1091                    {
1092                        for symbol in text.chars() {
1093                            let insert = !symbol.is_control()
1094                                && if let Some(filter) = self.filter.as_ref() {
1095                                    let filter = &mut *filter.lock();
1096                                    filter(symbol)
1097                                } else {
1098                                    true
1099                                };
1100                            if insert {
1101                                self.insert_char(symbol, ui);
1102                            }
1103                        }
1104                    }
1105                    WidgetMessage::KeyDown(code) => {
1106                        match code {
1107                            KeyCode::ArrowUp if !self.selecting => {
1108                                self.move_caret_y(-1, ui.keyboard_modifiers().shift);
1109                            }
1110                            KeyCode::ArrowDown if !self.selecting => {
1111                                self.move_caret_y(1, ui.keyboard_modifiers().shift);
1112                            }
1113                            KeyCode::ArrowRight if !self.selecting => {
1114                                if ui.keyboard_modifiers.control {
1115                                    self.move_caret(
1116                                        self.find_next_word(*self.caret_position),
1117                                        ui.keyboard_modifiers().shift,
1118                                    );
1119                                } else {
1120                                    self.move_caret_x(1, ui.keyboard_modifiers().shift);
1121                                }
1122                            }
1123                            KeyCode::ArrowLeft if !self.selecting => {
1124                                if ui.keyboard_modifiers.control {
1125                                    self.move_caret(
1126                                        self.find_prev_word(*self.caret_position),
1127                                        ui.keyboard_modifiers().shift,
1128                                    );
1129                                } else {
1130                                    self.move_caret_x(-1, ui.keyboard_modifiers().shift);
1131                                }
1132                            }
1133                            KeyCode::Delete
1134                                if !message.handled() && *self.editable && !self.selecting =>
1135                            {
1136                                self.remove_char(HorizontalDirection::Right, ui);
1137                            }
1138                            KeyCode::NumpadEnter | KeyCode::Enter if *self.editable => {
1139                                if *self.multiline {
1140                                    self.insert_char('\n', ui);
1141                                } else if *self.commit_mode == TextCommitMode::LostFocusPlusEnter {
1142                                    ui.send_message(TextMessage::text(
1143                                        self.handle,
1144                                        MessageDirection::FromWidget,
1145                                        self.text(),
1146                                    ));
1147                                } else if *self.commit_mode == TextCommitMode::Changed {
1148                                    self.commit_if_changed(ui);
1149                                }
1150                                // Don't set has_focus = false when enter is pressed.
1151                                // That messes up keyboard navigation.
1152                            }
1153                            KeyCode::Backspace if *self.editable && !self.selecting => {
1154                                self.remove_char(HorizontalDirection::Left, ui);
1155                            }
1156                            KeyCode::End if !self.selecting => {
1157                                let select = ui.keyboard_modifiers().shift;
1158                                let position = if ui.keyboard_modifiers().control {
1159                                    self.end_position()
1160                                } else {
1161                                    self.formatted_text
1162                                        .borrow()
1163                                        .get_line_range(self.caret_position.line)
1164                                        .end
1165                                };
1166                                self.move_caret(position, select);
1167                            }
1168                            KeyCode::Home if !self.selecting => {
1169                                let select = ui.keyboard_modifiers().shift;
1170                                let position = if ui.keyboard_modifiers().control {
1171                                    Position::default()
1172                                } else {
1173                                    self.formatted_text
1174                                        .borrow()
1175                                        .get_line_range(self.caret_position.line)
1176                                        .start
1177                                };
1178                                self.move_caret(position, select);
1179                            }
1180                            KeyCode::KeyA if ui.keyboard_modifiers().control => {
1181                                let end = self.end_position();
1182                                if end != Position::default() {
1183                                    self.selection_range.set_value_and_mark_modified(Some(
1184                                        SelectionRange {
1185                                            begin: Position::default(),
1186                                            end: self.end_position(),
1187                                        },
1188                                    ));
1189                                }
1190                            }
1191                            KeyCode::KeyC if ui.keyboard_modifiers().control => {
1192                                if let Some(mut clipboard) = ui.clipboard_mut() {
1193                                    if let Some(selection_range) = self.selection_range.as_ref() {
1194                                        let range = self
1195                                            .formatted_text
1196                                            .borrow()
1197                                            .position_range_to_char_index_range(
1198                                                selection_range.range(),
1199                                            );
1200                                        if !range.is_empty() {
1201                                            let _ = clipboard.set_contents(
1202                                                self.formatted_text.borrow().text_range(range),
1203                                            );
1204                                        }
1205                                    }
1206                                }
1207                            }
1208                            KeyCode::KeyV if ui.keyboard_modifiers().control => {
1209                                if let Some(mut clipboard) = ui.clipboard_mut() {
1210                                    if let Ok(content) = clipboard.get_contents() {
1211                                        self.insert_str(&content, ui);
1212                                    }
1213                                }
1214                            }
1215                            KeyCode::KeyX if ui.keyboard_modifiers().control => {
1216                                if let Some(mut clipboard) = ui.clipboard_mut() {
1217                                    if let Some(selection_range) = self.selection_range.as_ref() {
1218                                        let range = self
1219                                            .formatted_text
1220                                            .borrow()
1221                                            .position_range_to_char_index_range(
1222                                                selection_range.range(),
1223                                            );
1224                                        if !range.is_empty() {
1225                                            let _ = clipboard.set_contents(
1226                                                self.formatted_text.borrow().text_range(range),
1227                                            );
1228                                            self.remove_char(HorizontalDirection::Left, ui);
1229                                        }
1230                                    }
1231                                }
1232                            }
1233                            _ => (),
1234                        }
1235
1236                        // TextBox "eats" all input by default, some of the keys are used for input control while
1237                        // others are used directly to enter text.
1238                        message.set_handled(true);
1239                    }
1240                    WidgetMessage::Focus => {
1241                        if message.direction() == MessageDirection::FromWidget {
1242                            self.reset_blink();
1243                            self.has_focus = true;
1244                            let end = self.end_position();
1245                            if end != Position::default() {
1246                                self.set_caret_position(end);
1247                                self.selection_range.set_value_and_mark_modified(Some(
1248                                    SelectionRange {
1249                                        begin: Position::default(),
1250                                        end,
1251                                    },
1252                                ));
1253                            }
1254                            if *self.commit_mode == TextCommitMode::Changed {
1255                                self.recent.clear();
1256                                self.recent
1257                                    .extend_from_slice(self.formatted_text.borrow().get_raw_text());
1258                            }
1259                        }
1260                    }
1261                    WidgetMessage::Unfocus => {
1262                        if message.direction() == MessageDirection::FromWidget {
1263                            self.selection_range.set_value_and_mark_modified(None);
1264                            self.has_focus = false;
1265
1266                            match *self.commit_mode {
1267                                TextCommitMode::LostFocus | TextCommitMode::LostFocusPlusEnter => {
1268                                    ui.send_message(TextMessage::text(
1269                                        self.handle,
1270                                        MessageDirection::FromWidget,
1271                                        self.text(),
1272                                    ));
1273                                }
1274                                TextCommitMode::Changed => {
1275                                    self.commit_if_changed(ui);
1276                                }
1277                                _ => (),
1278                            }
1279                            // There is no reason to keep the stored recent value in memory
1280                            // while this TextBox does not have focus. Maybe this should be stored globally in UserInterface,
1281                            // since we only ever need one.
1282                            self.recent.clear();
1283                            self.recent.shrink_to(0);
1284                        }
1285                    }
1286                    WidgetMessage::MouseDown { pos, button } => {
1287                        if *button == MouseButton::Left {
1288                            let select = ui.keyboard_modifiers().shift;
1289                            if !select {
1290                                self.selection_range.set_value_and_mark_modified(None);
1291                            }
1292                            self.selecting = true;
1293                            self.has_focus = true;
1294                            self.before_click_position = *self.caret_position;
1295
1296                            if let Some(position) = self.screen_pos_to_text_pos(*pos) {
1297                                self.move_caret(position, select);
1298                            }
1299
1300                            ui.capture_mouse(self.handle());
1301                        }
1302                    }
1303                    WidgetMessage::DoubleClick {
1304                        button: MouseButton::Left,
1305                    } => {
1306                        if let Some(position) = self.screen_pos_to_text_pos(ui.cursor_position) {
1307                            if position == self.before_click_position {
1308                                self.select_word(position);
1309                            }
1310                        }
1311                    }
1312                    WidgetMessage::MouseMove { pos, .. } => {
1313                        if self.selecting {
1314                            if let Some(position) = self.screen_pos_to_text_pos(*pos) {
1315                                self.move_caret(position, true);
1316                            }
1317                        }
1318                    }
1319                    WidgetMessage::MouseUp { .. } => {
1320                        self.selecting = false;
1321                        ui.release_mouse_capture();
1322                    }
1323                    _ => {}
1324                }
1325            } else if let Some(msg) = message.data::<TextMessage>() {
1326                if message.direction() == MessageDirection::ToWidget {
1327                    let mut text = self.formatted_text.borrow_mut();
1328
1329                    match msg {
1330                        TextMessage::Text(new_text) => {
1331                            fn text_equals(
1332                                formatted_text: &FormattedText,
1333                                input_string: &str,
1334                            ) -> bool {
1335                                let raw_text = formatted_text.get_raw_text();
1336
1337                                if raw_text.len() != input_string.chars().count() {
1338                                    false
1339                                } else {
1340                                    for (raw_char, input_char) in
1341                                        raw_text.iter().zip(input_string.chars())
1342                                    {
1343                                        if *raw_char != input_char {
1344                                            return false;
1345                                        }
1346                                    }
1347
1348                                    true
1349                                }
1350                            }
1351                            self.selection_range.set_value_and_mark_modified(None);
1352                            if !text_equals(&text, new_text) {
1353                                text.set_text(new_text);
1354                                drop(text);
1355                                self.invalidate_layout();
1356                                self.formatted_text.borrow_mut().build();
1357
1358                                if *self.commit_mode == TextCommitMode::Immediate {
1359                                    ui.send_message(message.reverse());
1360                                }
1361                            }
1362                        }
1363                        TextMessage::Wrap(wrap_mode) => {
1364                            if text.wrap_mode() != *wrap_mode {
1365                                text.set_wrap(*wrap_mode);
1366                                drop(text);
1367                                self.invalidate_layout();
1368                                ui.send_message(message.reverse());
1369                            }
1370                        }
1371                        TextMessage::Font(font) => {
1372                            if &text.get_font() != font {
1373                                text.set_font(font.clone());
1374                                drop(text);
1375                                self.invalidate_layout();
1376                                ui.send_message(message.reverse());
1377                            }
1378                        }
1379                        TextMessage::VerticalAlignment(alignment) => {
1380                            if &text.vertical_alignment() != alignment {
1381                                text.set_vertical_alignment(*alignment);
1382                                drop(text);
1383                                self.invalidate_layout();
1384                                ui.send_message(message.reverse());
1385                            }
1386                        }
1387                        TextMessage::HorizontalAlignment(alignment) => {
1388                            if &text.horizontal_alignment() != alignment {
1389                                text.set_horizontal_alignment(*alignment);
1390                                drop(text);
1391                                self.invalidate_layout();
1392                                ui.send_message(message.reverse());
1393                            }
1394                        }
1395                        &TextMessage::Shadow(shadow) => {
1396                            if *text.shadow != shadow {
1397                                text.set_shadow(shadow);
1398                                drop(text);
1399                                self.invalidate_layout();
1400                                ui.send_message(message.reverse());
1401                            }
1402                        }
1403                        TextMessage::ShadowBrush(brush) => {
1404                            if &*text.shadow_brush != brush {
1405                                text.set_shadow_brush(brush.clone());
1406                                drop(text);
1407                                self.invalidate_layout();
1408                                ui.send_message(message.reverse());
1409                            }
1410                        }
1411                        &TextMessage::ShadowDilation(dilation) => {
1412                            if *text.shadow_dilation != dilation {
1413                                text.set_shadow_dilation(dilation);
1414                                drop(text);
1415                                self.invalidate_layout();
1416                                ui.send_message(message.reverse());
1417                            }
1418                        }
1419                        &TextMessage::ShadowOffset(offset) => {
1420                            if *text.shadow_offset != offset {
1421                                text.set_shadow_offset(offset);
1422                                drop(text);
1423                                self.invalidate_layout();
1424                                ui.send_message(message.reverse());
1425                            }
1426                        }
1427                        TextMessage::FontSize(height) => {
1428                            if text.font_size() != height {
1429                                text.set_font_size(height.clone());
1430                                drop(text);
1431                                self.invalidate_layout();
1432                                ui.send_message(message.reverse());
1433                            }
1434                        }
1435                    }
1436                }
1437            } else if let Some(msg) = message.data::<TextBoxMessage>() {
1438                if message.direction() == MessageDirection::ToWidget {
1439                    match msg {
1440                        TextBoxMessage::SelectionBrush(brush) => {
1441                            if &*self.selection_brush != brush {
1442                                self.selection_brush
1443                                    .set_value_and_mark_modified(brush.clone());
1444                                ui.send_message(message.reverse());
1445                            }
1446                        }
1447                        TextBoxMessage::CaretBrush(brush) => {
1448                            if &*self.caret_brush != brush {
1449                                self.caret_brush.set_value_and_mark_modified(brush.clone());
1450                                ui.send_message(message.reverse());
1451                            }
1452                        }
1453                        TextBoxMessage::TextCommitMode(mode) => {
1454                            if &*self.commit_mode != mode {
1455                                self.commit_mode.set_value_and_mark_modified(*mode);
1456                                ui.send_message(message.reverse());
1457                            }
1458                        }
1459                        TextBoxMessage::Multiline(multiline) => {
1460                            if &*self.multiline != multiline {
1461                                self.multiline.set_value_and_mark_modified(*multiline);
1462                                ui.send_message(message.reverse());
1463                            }
1464                        }
1465                        TextBoxMessage::Editable(editable) => {
1466                            if &*self.editable != editable {
1467                                self.editable.set_value_and_mark_modified(*editable);
1468                                ui.send_message(message.reverse());
1469                            }
1470                        }
1471                    }
1472                }
1473            }
1474        }
1475    }
1476}
1477
1478/// Text box builder creates new [`TextBox`] instances and adds them to the user interface.
1479pub struct TextBoxBuilder {
1480    widget_builder: WidgetBuilder,
1481    font: Option<FontResource>,
1482    text: String,
1483    caret_brush: Brush,
1484    selection_brush: Brush,
1485    filter: Option<Arc<Mutex<FilterCallback>>>,
1486    vertical_alignment: VerticalAlignment,
1487    horizontal_alignment: HorizontalAlignment,
1488    wrap: WrapMode,
1489    commit_mode: TextCommitMode,
1490    multiline: bool,
1491    editable: bool,
1492    mask_char: Option<char>,
1493    shadow: bool,
1494    shadow_brush: Brush,
1495    shadow_dilation: f32,
1496    shadow_offset: Vector2<f32>,
1497    skip_chars: Vec<char>,
1498    font_size: Option<StyledProperty<f32>>,
1499}
1500
1501impl TextBoxBuilder {
1502    /// Creates new text box widget builder with the base widget builder specified.
1503    pub fn new(widget_builder: WidgetBuilder) -> Self {
1504        Self {
1505            widget_builder,
1506            font: None,
1507            text: "".to_owned(),
1508            caret_brush: Brush::Solid(Color::WHITE),
1509            selection_brush: Brush::Solid(Color::opaque(80, 118, 178)),
1510            filter: None,
1511            vertical_alignment: VerticalAlignment::Top,
1512            horizontal_alignment: HorizontalAlignment::Left,
1513            wrap: WrapMode::NoWrap,
1514            commit_mode: TextCommitMode::LostFocusPlusEnter,
1515            multiline: false,
1516            editable: true,
1517            mask_char: None,
1518            shadow: false,
1519            shadow_brush: Brush::Solid(Color::BLACK),
1520            shadow_dilation: 1.0,
1521            shadow_offset: Vector2::new(1.0, 1.0),
1522            skip_chars: Default::default(),
1523            font_size: None,
1524        }
1525    }
1526
1527    /// Sets the desired font of the text box.
1528    pub fn with_font(mut self, font: FontResource) -> Self {
1529        self.font = Some(font);
1530        self
1531    }
1532
1533    /// Sets the desired text of the text box.
1534    pub fn with_text<P: AsRef<str>>(mut self, text: P) -> Self {
1535        text.as_ref().clone_into(&mut self.text);
1536        self
1537    }
1538
1539    /// Sets the desired caret brush of the text box.
1540    pub fn with_caret_brush(mut self, brush: Brush) -> Self {
1541        self.caret_brush = brush;
1542        self
1543    }
1544
1545    /// Sets the desired selection brush of the text box.
1546    pub fn with_selection_brush(mut self, brush: Brush) -> Self {
1547        self.selection_brush = brush;
1548        self
1549    }
1550
1551    /// Sets the desired character filter of the text box. See [`FilterCallback`] for more info.
1552    pub fn with_filter(mut self, filter: Arc<Mutex<FilterCallback>>) -> Self {
1553        self.filter = Some(filter);
1554        self
1555    }
1556
1557    /// Sets the desired vertical text alignment of the text box.
1558    pub fn with_vertical_text_alignment(mut self, alignment: VerticalAlignment) -> Self {
1559        self.vertical_alignment = alignment;
1560        self
1561    }
1562
1563    /// Sets the desired horizontal text alignment of the text box.
1564    pub fn with_horizontal_text_alignment(mut self, alignment: HorizontalAlignment) -> Self {
1565        self.horizontal_alignment = alignment;
1566        self
1567    }
1568
1569    /// Sets the desired word wrapping of the text box.
1570    pub fn with_wrap(mut self, wrap: WrapMode) -> Self {
1571        self.wrap = wrap;
1572        self
1573    }
1574
1575    /// Sets the desired text commit mode of the text box.
1576    pub fn with_text_commit_mode(mut self, mode: TextCommitMode) -> Self {
1577        self.commit_mode = mode;
1578        self
1579    }
1580
1581    /// Enables or disables multiline mode of the text box.
1582    pub fn with_multiline(mut self, multiline: bool) -> Self {
1583        self.multiline = multiline;
1584        self
1585    }
1586
1587    /// Enables or disables editing of the text box.
1588    pub fn with_editable(mut self, editable: bool) -> Self {
1589        self.editable = editable;
1590        self
1591    }
1592
1593    /// Sets the desired height of the text.
1594    pub fn with_font_size(mut self, font_size: StyledProperty<f32>) -> Self {
1595        self.font_size = Some(font_size);
1596        self
1597    }
1598
1599    /// Sets the desired masking character of the text box.
1600    pub fn with_mask_char(mut self, mask_char: Option<char>) -> Self {
1601        self.mask_char = mask_char;
1602        self
1603    }
1604
1605    /// Whether the shadow enabled or not.
1606    pub fn with_shadow(mut self, shadow: bool) -> Self {
1607        self.shadow = shadow;
1608        self
1609    }
1610
1611    /// Sets desired shadow brush. It will be used to render the shadow.
1612    pub fn with_shadow_brush(mut self, brush: Brush) -> Self {
1613        self.shadow_brush = brush;
1614        self
1615    }
1616
1617    /// Sets desired shadow dilation in units. Keep in mind that the dilation is absolute,
1618    /// not percentage-based.
1619    pub fn with_shadow_dilation(mut self, thickness: f32) -> Self {
1620        self.shadow_dilation = thickness;
1621        self
1622    }
1623
1624    /// Sets desired shadow offset in units.
1625    pub fn with_shadow_offset(mut self, offset: Vector2<f32>) -> Self {
1626        self.shadow_offset = offset;
1627        self
1628    }
1629
1630    /// Sets desired set of characters that will be treated like whitespace during Ctrl+Arrow navigation
1631    /// (Ctrl+Left Arrow and Ctrl+Right Arrow). This could be useful to treat underscores like whitespaces,
1632    /// which in its turn could be useful for in-game consoles where commands usually separated using
1633    /// underscores (`like_this_one`).
1634    pub fn with_skip_chars(mut self, chars: Vec<char>) -> Self {
1635        self.skip_chars = chars;
1636        self
1637    }
1638
1639    /// Creates a new [`TextBox`] instance and adds it to the user interface.
1640    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
1641        let style = &ctx.style;
1642
1643        if self.widget_builder.foreground.is_none() {
1644            self.widget_builder.foreground = Some(style.property(Style::BRUSH_TEXT));
1645        }
1646        if self.widget_builder.background.is_none() {
1647            self.widget_builder.background = Some(style.property(Style::BRUSH_DARKER));
1648        }
1649        if self.widget_builder.cursor.is_none() {
1650            self.widget_builder.cursor = Some(CursorIcon::Text);
1651        }
1652
1653        let text_box = TextBox {
1654            widget: self
1655                .widget_builder
1656                .with_accepts_input(true)
1657                .with_need_update(true)
1658                .build(ctx),
1659            caret_position: Position::default().into(),
1660            caret_visible: false.into(),
1661            blink_timer: 0.0.into(),
1662            blink_interval: 0.5.into(),
1663            formatted_text: RefCell::new(
1664                FormattedTextBuilder::new(self.font.unwrap_or_else(|| ctx.default_font()))
1665                    .with_text(self.text)
1666                    .with_horizontal_alignment(self.horizontal_alignment)
1667                    .with_vertical_alignment(self.vertical_alignment)
1668                    .with_wrap(self.wrap)
1669                    .with_mask_char(self.mask_char)
1670                    .with_shadow(self.shadow)
1671                    .with_shadow_brush(self.shadow_brush)
1672                    .with_shadow_dilation(self.shadow_dilation)
1673                    .with_shadow_offset(self.shadow_offset)
1674                    .with_font_size(
1675                        self.font_size
1676                            .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
1677                    )
1678                    .build(),
1679            ),
1680            selection_range: None.into(),
1681            selecting: false,
1682            before_click_position: Position::default(),
1683            selection_brush: self.selection_brush.into(),
1684            caret_brush: self.caret_brush.into(),
1685            has_focus: false,
1686            filter: self.filter,
1687            commit_mode: self.commit_mode.into(),
1688            multiline: self.multiline.into(),
1689            editable: self.editable.into(),
1690            view_position: Default::default(),
1691            skip_chars: self.skip_chars.into(),
1692            recent: Default::default(),
1693        };
1694
1695        ctx.add_node(UiNode::new(text_box))
1696    }
1697}
1698
1699#[cfg(test)]
1700mod test {
1701    use crate::text_box::TextBoxBuilder;
1702    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1703
1704    #[test]
1705    fn test_deletion() {
1706        test_widget_deletion(|ctx| TextBoxBuilder::new(WidgetBuilder::new()).build(ctx));
1707    }
1708}