diff options
author | 2023-03-04 05:37:11 +0100 | |
---|---|---|
committer | 2023-03-04 05:37:11 +0100 | |
commit | 3a0d34c0240f4421737a6a08761f99d6f8140d02 (patch) | |
tree | c9a4a6b8e9c1db1b8fcd05bc98e3f131d5ef4bd5 /native/src/widget/text_input.rs | |
parent | c54409d1711e1f615c7ea4b02c082954e340632a (diff) | |
download | iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.tar.gz iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.tar.bz2 iced-3a0d34c0240f4421737a6a08761f99d6f8140d02.zip |
Create `iced_widget` subcrate and re-organize the whole codebase
Diffstat (limited to 'native/src/widget/text_input.rs')
-rw-r--r-- | native/src/widget/text_input.rs | 1218 |
1 files changed, 0 insertions, 1218 deletions
diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs deleted file mode 100644 index 65a9bd3b..00000000 --- a/native/src/widget/text_input.rs +++ /dev/null @@ -1,1218 +0,0 @@ -//! Display fields that can be filled with text. -//! -//! A [`TextInput`] has some local [`State`]. -mod editor; -mod value; - -pub mod cursor; - -pub use cursor::Cursor; -pub use value::Value; - -use editor::Editor; - -use crate::alignment; -use crate::event::{self, Event}; -use crate::keyboard; -use crate::layout; -use crate::mouse::{self, click}; -use crate::renderer; -use crate::text::{self, Text}; -use crate::time::{Duration, Instant}; -use crate::touch; -use crate::widget; -use crate::widget::operation::{self, Operation}; -use crate::widget::tree::{self, Tree}; -use crate::window; -use crate::{ - Clipboard, Color, Command, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, -}; - -pub use iced_style::text_input::{Appearance, StyleSheet}; - -/// A field that can be filled with text. -/// -/// # Example -/// ``` -/// # pub type TextInput<'a, Message> = iced_native::widget::TextInput<'a, Message, iced_native::renderer::Null>; -/// #[derive(Debug, Clone)] -/// enum Message { -/// TextInputChanged(String), -/// } -/// -/// let value = "Some text"; -/// -/// let input = TextInput::new( -/// "This is the placeholder...", -/// value, -/// Message::TextInputChanged, -/// ) -/// .padding(10); -/// ``` -///  -#[allow(missing_debug_implementations)] -pub struct TextInput<'a, Message, Renderer> -where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - id: Option<Id>, - placeholder: String, - value: Value, - is_secure: bool, - font: Option<Renderer::Font>, - width: Length, - padding: Padding, - size: Option<f32>, - on_change: Box<dyn Fn(String) -> Message + 'a>, - on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, - on_submit: Option<Message>, - style: <Renderer::Theme as StyleSheet>::Style, -} - -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - /// Creates a new [`TextInput`]. - /// - /// It expects: - /// - a placeholder, - /// - the current value, and - /// - a function that produces a message when the [`TextInput`] changes. - pub fn new<F>(placeholder: &str, value: &str, on_change: F) -> Self - where - F: 'a + Fn(String) -> Message, - { - TextInput { - id: None, - placeholder: String::from(placeholder), - value: Value::new(value), - is_secure: false, - font: None, - width: Length::Fill, - padding: Padding::new(5.0), - size: None, - on_change: Box::new(on_change), - on_paste: None, - on_submit: None, - style: Default::default(), - } - } - - /// Sets the [`Id`] of the [`TextInput`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// Converts the [`TextInput`] into a secure password input. - pub fn password(mut self) -> Self { - self.is_secure = true; - self - } - - /// Sets the message that should be produced when some text is pasted into - /// the [`TextInput`]. - pub fn on_paste( - mut self, - on_paste: impl Fn(String) -> Message + 'a, - ) -> Self { - self.on_paste = Some(Box::new(on_paste)); - self - } - - /// Sets the [`Font`] of the [`TextInput`]. - /// - /// [`Font`]: text::Renderer::Font - pub fn font(mut self, font: Renderer::Font) -> Self { - self.font = Some(font); - self - } - /// Sets the width of the [`TextInput`]. - pub fn width(mut self, width: impl Into<Length>) -> Self { - self.width = width.into(); - self - } - - /// Sets the [`Padding`] of the [`TextInput`]. - pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { - self.padding = padding.into(); - self - } - - /// Sets the text size of the [`TextInput`]. - pub fn size(mut self, size: impl Into<Pixels>) -> Self { - self.size = Some(size.into().0); - self - } - - /// Sets the message that should be produced when the [`TextInput`] is - /// focused and the enter key is pressed. - pub fn on_submit(mut self, message: Message) -> Self { - self.on_submit = Some(message); - self - } - - /// Sets the style of the [`TextInput`]. - pub fn style( - mut self, - style: impl Into<<Renderer::Theme as StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); - self - } - - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. - /// - /// [`Renderer`]: text::Renderer - pub fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - value: Option<&Value>, - ) { - draw( - renderer, - theme, - layout, - cursor_position, - tree.state.downcast_ref::<State>(), - value.unwrap_or(&self.value), - &self.placeholder, - self.size, - self.font, - self.is_secure, - &self.style, - ) - } -} - -impl<'a, Message, Renderer> Widget<Message, Renderer> - for TextInput<'a, Message, Renderer> -where - Message: Clone, - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn tag(&self) -> tree::Tag { - tree::Tag::of::<State>() - } - - fn state(&self) -> tree::State { - tree::State::new(State::new()) - } - - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - layout(renderer, limits, self.width, self.padding, self.size) - } - - fn operate( - &self, - tree: &mut Tree, - _layout: Layout<'_>, - _renderer: &Renderer, - operation: &mut dyn Operation<Message>, - ) { - let state = tree.state.downcast_mut::<State>(); - - operation.focusable(state, self.id.as_ref().map(|id| &id.0)); - operation.text_input(state, self.id.as_ref().map(|id| &id.0)); - } - - fn on_event( - &mut self, - tree: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - update( - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - &mut self.value, - self.size, - self.font, - self.is_secure, - self.on_change.as_ref(), - self.on_paste.as_deref(), - &self.on_submit, - || tree.state.downcast_mut::<State>(), - ) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - ) { - draw( - renderer, - theme, - layout, - cursor_position, - tree.state.downcast_ref::<State>(), - &self.value, - &self.placeholder, - self.size, - self.font, - self.is_secure, - &self.style, - ) - } - - fn mouse_interaction( - &self, - _state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor_position) - } -} - -impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>> - for Element<'a, Message, Renderer> -where - Message: 'a + Clone, - Renderer: 'a + text::Renderer, - Renderer::Theme: StyleSheet, -{ - fn from( - text_input: TextInput<'a, Message, Renderer>, - ) -> Element<'a, Message, Renderer> { - Element::new(text_input) - } -} - -/// The identifier of a [`TextInput`]. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(widget::Id); - -impl Id { - /// Creates a custom [`Id`]. - pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self { - Self(widget::Id::new(id)) - } - - /// Creates a unique [`Id`]. - /// - /// This function produces a different [`Id`] every time it is called. - pub fn unique() -> Self { - Self(widget::Id::unique()) - } -} - -impl From<Id> for widget::Id { - fn from(id: Id) -> Self { - id.0 - } -} - -/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::focusable::focus(id.0)) -} - -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the -/// end. -pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to_end(id.0)) -} - -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the -/// front. -pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to_front(id.0)) -} - -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the -/// provided position. -pub fn move_cursor_to<Message: 'static>( - id: Id, - position: usize, -) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to(id.0, position)) -} - -/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::select_all(id.0)) -} - -/// Computes the layout of a [`TextInput`]. -pub fn layout<Renderer>( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - size: Option<f32>, -) -> layout::Node -where - Renderer: text::Renderer, -{ - let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let limits = limits.width(width).pad(padding).height(text_size * 1.2); - - let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(text.size().pad(padding), vec![text]) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] -/// accordingly. -pub fn update<'a, Message, Renderer>( - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - value: &mut Value, - size: Option<f32>, - font: Option<Renderer::Font>, - is_secure: bool, - on_change: &dyn Fn(String) -> Message, - on_paste: Option<&dyn Fn(String) -> Message>, - on_submit: &Option<Message>, - state: impl FnOnce() -> &'a mut State, -) -> event::Status -where - Message: Clone, - Renderer: text::Renderer, -{ - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - let is_clicked = layout.bounds().contains(cursor_position); - - state.is_focused = if is_clicked { - state.is_focused.or_else(|| { - let now = Instant::now(); - - Some(Focus { - updated_at: now, - now, - }) - }) - } else { - None - }; - - if is_clicked { - let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; - - let click = - mouse::Click::new(cursor_position, state.last_click); - - match click.kind() { - click::Kind::Single => { - let position = if target > 0.0 { - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - ) - } else { - None - } - .unwrap_or(0); - - if state.keyboard_modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - position, - ); - } else { - state.cursor.move_to(position); - } - state.is_dragging = true; - } - click::Kind::Double => { - if is_secure { - state.cursor.select_all(value); - } else { - let position = find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - value, - state, - target, - ) - .unwrap_or(0); - - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); - } - - state.is_dragging = false; - } - click::Kind::Triple => { - state.cursor.select_all(value); - state.is_dragging = false; - } - } - - state.last_click = Some(click); - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state().is_dragging = false; - } - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - let state = state(); - - if state.is_dragging { - let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; - - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - let position = find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - ) - .unwrap_or(0); - - state - .cursor - .select_range(state.cursor.start(&value), position); - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - if state.is_pasting.is_none() - && !state.keyboard_modifiers.command() - && !c.is_control() - { - let mut editor = Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_change)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - return event::Status::Captured; - } - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); - - match key_code { - keyboard::KeyCode::Enter - | keyboard::KeyCode::NumpadEnter => { - if let Some(on_submit) = on_submit.clone() { - shell.publish(on_submit); - } - } - keyboard::KeyCode::Backspace => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state.cursor.select_range(0, cursor_pos); - } else { - state.cursor.select_left_by_words(value); - } - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.backspace(); - - let message = (on_change)(editor.contents()); - shell.publish(message); - } - keyboard::KeyCode::Delete => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state - .cursor - .select_range(cursor_pos, value.len()); - } else { - state.cursor.select_right_by_words(value); - } - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_change)(editor.contents()); - shell.publish(message); - } - keyboard::KeyCode::Left => { - if platform::is_jump_modifier_pressed(modifiers) - && !is_secure - { - if modifiers.shift() { - state.cursor.select_left_by_words(value); - } else { - state.cursor.move_left_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_left(value) - } else { - state.cursor.move_left(value); - } - } - keyboard::KeyCode::Right => { - if platform::is_jump_modifier_pressed(modifiers) - && !is_secure - { - if modifiers.shift() { - state.cursor.select_right_by_words(value); - } else { - state.cursor.move_right_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_right(value) - } else { - state.cursor.move_right(value); - } - } - keyboard::KeyCode::Home => { - if modifiers.shift() { - state - .cursor - .select_range(state.cursor.start(value), 0); - } else { - state.cursor.move_to(0); - } - } - keyboard::KeyCode::End => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - value.len(), - ); - } else { - state.cursor.move_to(value.len()); - } - } - keyboard::KeyCode::C - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard - .write(value.select(start, end).to_string()); - } - } - keyboard::KeyCode::X - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard - .write(value.select(start, end).to_string()); - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_change)(editor.contents()); - shell.publish(message); - } - keyboard::KeyCode::V => { - if state.keyboard_modifiers.command() { - let content = match state.is_pasting.take() { - Some(content) => content, - None => { - let content: String = clipboard - .read() - .unwrap_or_default() - .chars() - .filter(|c| !c.is_control()) - .collect(); - - Value::new(&content) - } - }; - - let mut editor = - Editor::new(value, &mut state.cursor); - - editor.paste(content.clone()); - - let message = if let Some(paste) = &on_paste { - (paste)(editor.contents()) - } else { - (on_change)(editor.contents()) - }; - shell.publish(message); - - state.is_pasting = Some(content); - } else { - state.is_pasting = None; - } - } - keyboard::KeyCode::A - if state.keyboard_modifiers.command() => - { - state.cursor.select_all(value); - } - keyboard::KeyCode::Escape => { - state.is_focused = None; - state.is_dragging = false; - state.is_pasting = None; - - state.keyboard_modifiers = - keyboard::Modifiers::default(); - } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { - let state = state(); - - if state.is_focused.is_some() { - match key_code { - keyboard::KeyCode::V => { - state.is_pasting = None; - } - keyboard::KeyCode::Tab - | keyboard::KeyCode::Up - | keyboard::KeyCode::Down => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } else { - state.is_pasting = None; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - } - Event::Window(window::Event::RedrawRequested(now)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.now = now; - - let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() - % CURSOR_BLINK_INTERVAL_MILLIS; - - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis(millis_until_redraw as u64), - )); - } - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws the [`TextInput`] with the given [`Renderer`], overriding its -/// [`Value`] if provided. -/// -/// [`Renderer`]: text::Renderer -pub fn draw<Renderer>( - renderer: &mut Renderer, - theme: &Renderer::Theme, - layout: Layout<'_>, - cursor_position: Point, - state: &State, - value: &Value, - placeholder: &str, - size: Option<f32>, - font: Option<Renderer::Font>, - is_secure: bool, - style: &<Renderer::Theme as StyleSheet>::Style, -) where - Renderer: text::Renderer, - Renderer::Theme: StyleSheet, -{ - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); - - let is_mouse_over = bounds.contains(cursor_position); - - let appearance = if state.is_focused() { - theme.focused(style) - } else if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: appearance.border_radius.into(), - border_width: appearance.border_width, - border_color: appearance.border_color, - }, - appearance.background, - ); - - let text = value.to_string(); - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); - - let (cursor, offset) = if let Some(focus) = &state.is_focused { - match state.cursor.state(value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - position, - font, - ); - - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; - - let cursor = if is_cursor_visible { - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - theme.value_color(style), - )) - } else { - None - }; - - (cursor, offset) - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - let (left_position, left_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - left, - font, - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - right, - font, - ); - - let width = right_position - left_position; - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + left_position, - y: text_bounds.y, - width, - height: text_bounds.height, - }, - border_radius: 0.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - theme.selection_color(style), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let text_width = renderer.measure_width( - if text.is_empty() { placeholder } else { &text }, - size, - font, - ); - - let render = |renderer: &mut Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); - } - - renderer.fill_text(Text { - content: if text.is_empty() { placeholder } else { &text }, - color: if text.is_empty() { - theme.placeholder_color(style) - } else { - theme.value_color(style) - }, - font, - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - }; - - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render) - }); - } else { - render(renderer); - } -} - -/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor_position: Point, -) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Text - } else { - mouse::Interaction::default() - } -} - -/// The state of a [`TextInput`]. -#[derive(Debug, Default, Clone)] -pub struct State { - is_focused: Option<Focus>, - is_dragging: bool, - is_pasting: Option<Value>, - last_click: Option<mouse::Click>, - cursor: Cursor, - keyboard_modifiers: keyboard::Modifiers, - // TODO: Add stateful horizontal scrolling offset -} - -#[derive(Debug, Clone, Copy)] -struct Focus { - updated_at: Instant, - now: Instant, -} - -impl State { - /// Creates a new [`State`], representing an unfocused [`TextInput`]. - pub fn new() -> Self { - Self::default() - } - - /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused() -> Self { - Self { - is_focused: None, - is_dragging: false, - is_pasting: None, - last_click: None, - cursor: Cursor::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - } - } - - /// Returns whether the [`TextInput`] is currently focused or not. - pub fn is_focused(&self) -> bool { - self.is_focused.is_some() - } - - /// Returns the [`Cursor`] of the [`TextInput`]. - pub fn cursor(&self) -> Cursor { - self.cursor - } - - /// Focuses the [`TextInput`]. - pub fn focus(&mut self) { - let now = Instant::now(); - - self.is_focused = Some(Focus { - updated_at: now, - now, - }); - - self.move_cursor_to_end(); - } - - /// Unfocuses the [`TextInput`]. - pub fn unfocus(&mut self) { - self.is_focused = None; - } - - /// Moves the [`Cursor`] of the [`TextInput`] to the front of the input text. - pub fn move_cursor_to_front(&mut self) { - self.cursor.move_to(0); - } - - /// Moves the [`Cursor`] of the [`TextInput`] to the end of the input text. - pub fn move_cursor_to_end(&mut self) { - self.cursor.move_to(usize::MAX); - } - - /// Moves the [`Cursor`] of the [`TextInput`] to an arbitrary location. - pub fn move_cursor_to(&mut self, position: usize) { - self.cursor.move_to(position); - } - - /// Selects all the content of the [`TextInput`]. - pub fn select_all(&mut self) { - self.cursor.select_range(0, usize::MAX); - } -} - -impl operation::Focusable for State { - fn is_focused(&self) -> bool { - State::is_focused(self) - } - - fn focus(&mut self) { - State::focus(self) - } - - fn unfocus(&mut self) { - State::unfocus(self) - } -} - -impl operation::TextInput for State { - fn move_cursor_to_front(&mut self) { - State::move_cursor_to_front(self) - } - - fn move_cursor_to_end(&mut self) { - State::move_cursor_to_end(self) - } - - fn move_cursor_to(&mut self, position: usize) { - State::move_cursor_to(self, position) - } - - fn select_all(&mut self) { - State::select_all(self) - } -} - -mod platform { - use crate::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } -} - -fn offset<Renderer>( - renderer: &Renderer, - text_bounds: Rectangle, - font: Renderer::Font, - size: f32, - value: &Value, - state: &State, -) -> f32 -where - Renderer: text::Renderer, -{ - if state.is_focused() { - let cursor = state.cursor(); - - let focus_position = match cursor.state(value) { - cursor::State::Index(i) => i, - cursor::State::Selection { end, .. } => end, - }; - - let (_, offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - focus_position, - font, - ); - - offset - } else { - 0.0 - } -} - -fn measure_cursor_and_scroll_offset<Renderer>( - renderer: &Renderer, - text_bounds: Rectangle, - value: &Value, - size: f32, - cursor_index: usize, - font: Renderer::Font, -) -> (f32, f32) -where - Renderer: text::Renderer, -{ - let text_before_cursor = value.until(cursor_index).to_string(); - - let text_value_width = - renderer.measure_width(&text_before_cursor, size, font); - - let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); - - (text_value_width, offset) -} - -/// Computes the position of the text cursor at the given X coordinate of -/// a [`TextInput`]. -fn find_cursor_position<Renderer>( - renderer: &Renderer, - text_bounds: Rectangle, - font: Option<Renderer::Font>, - size: Option<f32>, - value: &Value, - state: &State, - x: f32, -) -> Option<usize> -where - Renderer: text::Renderer, -{ - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); - - let offset = offset(renderer, text_bounds, font, size, value, state); - let value = value.to_string(); - - let char_offset = renderer - .hit_test( - &value, - size, - font, - Size::INFINITY, - Point::new(x + offset, text_bounds.height / 2.0), - true, - ) - .map(text::Hit::cursor)?; - - Some( - unicode_segmentation::UnicodeSegmentation::graphemes( - &value[..char_offset], - true, - ) - .count(), - ) -} - -const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; |