diff options
-rw-r--r-- | style/src/lib.rs | 1 | ||||
-rw-r--r-- | style/src/text_input.rs | 45 | ||||
-rw-r--r-- | style/src/theme.rs | 125 | ||||
-rw-r--r-- | widget/src/combo_box.rs | 27 | ||||
-rw-r--r-- | widget/src/helpers.rs | 8 | ||||
-rw-r--r-- | widget/src/overlay/menu.rs | 4 | ||||
-rw-r--r-- | widget/src/pick_list.rs | 14 | ||||
-rw-r--r-- | widget/src/scrollable.rs | 18 | ||||
-rw-r--r-- | widget/src/text_input.rs | 1669 |
9 files changed, 855 insertions, 1056 deletions
diff --git a/style/src/lib.rs b/style/src/lib.rs index da3c0676..5906fa0e 100644 --- a/style/src/lib.rs +++ b/style/src/lib.rs @@ -27,7 +27,6 @@ pub mod rule; pub mod slider; pub mod svg; pub mod text_editor; -pub mod text_input; pub mod theme; pub mod toggler; diff --git a/style/src/text_input.rs b/style/src/text_input.rs deleted file mode 100644 index 8ba9957f..00000000 --- a/style/src/text_input.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Change the appearance of a text input. -use iced_core::{Background, Border, Color}; - -/// The appearance of a text input. -#[derive(Debug, Clone, Copy)] -pub struct Appearance { - /// The [`Background`] of the text input. - pub background: Background, - /// The [`Border`] of the text input. - pub border: Border, - /// The icon [`Color`] of the text input. - pub icon_color: Color, -} - -/// A set of rules that dictate the style of a text input. -pub trait StyleSheet { - /// The supported style of the [`StyleSheet`]. - type Style: Default; - - /// Produces the style of an active text input. - fn active(&self, style: &Self::Style) -> Appearance; - - /// Produces the style of a focused text input. - fn focused(&self, style: &Self::Style) -> Appearance; - - /// Produces the [`Color`] of the placeholder of a text input. - fn placeholder_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the value of a text input. - fn value_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the value of a disabled text input. - fn disabled_color(&self, style: &Self::Style) -> Color; - - /// Produces the [`Color`] of the selection of a text input. - fn selection_color(&self, style: &Self::Style) -> Color; - - /// Produces the style of an hovered text input. - fn hovered(&self, style: &Self::Style) -> Appearance { - self.focused(style) - } - - /// Produces the style of a disabled text input. - fn disabled(&self, style: &Self::Style) -> Appearance; -} diff --git a/style/src/theme.rs b/style/src/theme.rs index 6fee19ff..57dfe3c7 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -15,7 +15,6 @@ use crate::rule; use crate::slider; use crate::svg; use crate::text_editor; -use crate::text_input; use crate::toggler; use crate::core::{Background, Border, Color}; @@ -795,130 +794,6 @@ impl text::StyleSheet for Theme {} /// The style of a text input. #[derive(Default)] -pub enum TextInput { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn text_input::StyleSheet<Style = Theme>>), -} - -impl text_input::StyleSheet for Theme { - type Style = TextInput; - - fn active(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.active(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - icon_color: palette.background.weak.text, - } - } - - fn hovered(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.hovered(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.base.text, - }, - icon_color: palette.background.weak.text, - } - } - - fn focused(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.focused(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - icon_color: palette.background.weak.text, - } - } - - fn placeholder_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.placeholder_color(self); - } - - let palette = self.extended_palette(); - - palette.background.strong.color - } - - fn value_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.value_color(self); - } - - let palette = self.extended_palette(); - - palette.background.base.text - } - - fn selection_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.selection_color(self); - } - - let palette = self.extended_palette(); - - palette.primary.weak.color - } - - fn disabled(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.disabled(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.weak.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - icon_color: palette.background.strong.color, - } - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.disabled_color(self); - } - - self.placeholder_color(style) - } -} - -/// The style of a text input. -#[derive(Default)] pub enum TextEditor { /// The default style. #[default] diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 0cca8d56..2ecf799d 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -32,7 +32,7 @@ pub struct ComboBox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: text_input::StyleSheet + menu::StyleSheet, + Theme: text_input::Style + menu::StyleSheet, Renderer: text::Renderer, { state: &'a State<T>, @@ -51,7 +51,7 @@ pub struct ComboBox< impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: text_input::StyleSheet + menu::StyleSheet, + Theme: text_input::Style + menu::StyleSheet, Renderer: text::Renderer, { /// Creates a new [`ComboBox`] with the given list of options, a placeholder, @@ -121,20 +121,17 @@ where // TODO: Define its own `StyleSheet` trait pub fn style<S>(mut self, style: S) -> Self where - S: Into<<Theme as text_input::StyleSheet>::Style> - + Into<<Theme as menu::StyleSheet>::Style> - + Clone, + S: Into<<Theme as menu::StyleSheet>::Style>, { - self.menu_style = style.clone().into(); - self.text_input = self.text_input.style(style); + self.menu_style = style.into(); self } /// Sets the style of the [`TextInput`] of the [`ComboBox`]. - pub fn text_input_style<S>(mut self, style: S) -> Self - where - S: Into<<Theme as text_input::StyleSheet>::Style> + Clone, - { + pub fn text_input_style( + mut self, + style: fn(&Theme, text_input::Status) -> text_input::Appearance, + ) -> Self { self.text_input = self.text_input.style(style); self } @@ -300,8 +297,8 @@ where T: Display + Clone + 'static, Message: Clone, Theme: container::Style - + text_input::StyleSheet - + scrollable::Tradition + + text_input::Style + + scrollable::Style + menu::StyleSheet, Renderer: text::Renderer, { @@ -720,8 +717,8 @@ where T: Display + Clone + 'static, Message: Clone + 'a, Theme: container::Style - + text_input::StyleSheet - + scrollable::Tradition + + text_input::Style + + scrollable::Style + menu::StyleSheet + 'a, Renderer: text::Renderer + 'a, diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3274b8d2..2153ed50 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -104,7 +104,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Scrollable<'a, Message, Theme, Renderer> where - Theme: scrollable::Tradition, + Theme: scrollable::Style, Renderer: core::Renderer, { Scrollable::new(content) @@ -209,7 +209,7 @@ pub fn text_input<'a, Message, Theme, Renderer>( ) -> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: text_input::StyleSheet, + Theme: text_input::Style, Renderer: core::text::Renderer, { TextInput::new(placeholder, value) @@ -276,7 +276,7 @@ where Message: Clone, Renderer: core::text::Renderer, Theme: pick_list::StyleSheet - + scrollable::Tradition + + scrollable::Style + overlay::menu::StyleSheet + container::Style, <Theme as overlay::menu::StyleSheet>::Style: @@ -296,7 +296,7 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>( ) -> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: text_input::StyleSheet + overlay::menu::StyleSheet, + Theme: text_input::Style + overlay::menu::StyleSheet, Renderer: core::text::Renderer, { ComboBox::new(state, placeholder, selection, on_selected) diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index b9b735e4..d820592d 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -47,7 +47,7 @@ impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + container::Style + scrollable::Tradition + 'a, + Theme: StyleSheet + container::Style + scrollable::Style + 'a, Renderer: text::Renderer + 'a, { /// Creates a new [`Menu`] with the given [`State`], a list of options, and @@ -179,7 +179,7 @@ where impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + container::Style + scrollable::Tradition + 'a, + Theme: StyleSheet + container::Style + scrollable::Style + 'a, Renderer: text::Renderer + 'a, { pub fn new<T>( diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index b75baa74..aeb0f246 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -61,10 +61,7 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone, - Theme: StyleSheet - + scrollable::Tradition - + menu::StyleSheet - + container::Style, + Theme: StyleSheet + scrollable::Style + menu::StyleSheet + container::Style, <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, Renderer: text::Renderer, { @@ -176,10 +173,7 @@ where L: Borrow<[T]>, V: Borrow<T>, Message: Clone + 'a, - Theme: StyleSheet - + scrollable::Tradition - + menu::StyleSheet - + container::Style, + Theme: StyleSheet + scrollable::Style + menu::StyleSheet + container::Style, <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, Renderer: text::Renderer + 'a, { @@ -318,7 +312,7 @@ where V: Borrow<T> + 'a, Message: Clone + 'a, Theme: StyleSheet - + scrollable::Tradition + + scrollable::Style + menu::StyleSheet + container::Style + 'a, @@ -628,7 +622,7 @@ where T: Clone + ToString, Message: 'a, Theme: StyleSheet - + scrollable::Tradition + + scrollable::Style + menu::StyleSheet + container::Style + 'a, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 8231685b..864fbec8 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -49,7 +49,7 @@ where content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Self where - Theme: Tradition, + Theme: Style, { Self::with_direction(content, Direction::default()) } @@ -60,7 +60,7 @@ where direction: Direction, ) -> Self where - Theme: Tradition, + Theme: Style, { let content = content.into(); @@ -83,7 +83,7 @@ where direction, content, on_scroll: None, - style: Theme::tradition(), + style: Theme::style(), } } @@ -1653,14 +1653,14 @@ pub struct Scroller { pub border: Border, } -/// The definition of the traditional style of a [`Scrollable`]. -pub trait Tradition { - /// Returns the traditional style of a [`Scrollable`]. - fn tradition() -> fn(&Self, Status) -> Appearance; +/// The definition of the default style of a [`Scrollable`]. +pub trait Style { + /// Returns the default style of a [`Scrollable`]. + fn style() -> fn(&Self, Status) -> Appearance; } -impl Tradition for Theme { - fn tradition() -> fn(&Self, Status) -> Appearance { +impl Style for Theme { + fn style() -> fn(&Self, Status) -> Appearance { default } } diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 92c4892c..11b0a5d5 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -27,12 +27,11 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, Vector, Widget, }; use crate::runtime::Command; - -pub use iced_style::text_input::{Appearance, StyleSheet}; +use crate::style::Theme; /// A field that can be filled with text. /// @@ -63,7 +62,6 @@ pub struct TextInput< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { id: Option<Id>, @@ -79,7 +77,7 @@ pub struct TextInput< on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, icon: Option<Icon<Renderer::Font>>, - style: Theme::Style, + style: fn(&Theme, Status) -> Appearance, } /// The default [`Padding`] of a [`TextInput`]. @@ -88,7 +86,6 @@ pub const DEFAULT_PADDING: Padding = Padding::new(5.0); impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { /// Creates a new [`TextInput`]. @@ -96,7 +93,10 @@ where /// It expects: /// - a placeholder, /// - the current value - pub fn new(placeholder: &str, value: &str) -> Self { + pub fn new(placeholder: &str, value: &str) -> Self + where + Theme: Style, + { TextInput { id: None, placeholder: String::from(placeholder), @@ -111,7 +111,7 @@ where on_paste: None, on_submit: None, icon: None, - style: Default::default(), + style: Theme::style(), } } @@ -198,8 +198,8 @@ where } /// Sets the style of the [`TextInput`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; self } @@ -213,20 +213,90 @@ where limits: &layout::Limits, value: Option<&Value>, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.font, - self.line_height, - self.icon.as_ref(), - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - value.unwrap_or(&self.value), - &self.placeholder, - self.is_secure, - ) + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + let value = value.unwrap_or(&self.value); + + let font = self.font.unwrap_or_else(|| renderer.default_font()); + let text_size = self.size.unwrap_or_else(|| renderer.default_size()); + let padding = self.padding.fit(Size::ZERO, limits.max()); + let height = self.line_height.to_absolute(text_size); + + let limits = limits.width(self.width).shrink(padding); + let text_bounds = limits.resolve(self.width, height, Size::ZERO); + + let placeholder_text = Text { + font, + line_height: self.line_height, + content: &self.placeholder, + bounds: Size::new(f32::INFINITY, text_bounds.height), + size: text_size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + state.placeholder.update(placeholder_text); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); + + if let Some(icon) = &self.icon { + let icon_text = Text { + line_height: self.line_height, + content: &icon.code_point.to_string(), + font: icon.font, + size: icon.size.unwrap_or_else(|| renderer.default_size()), + bounds: Size::new(f32::INFINITY, text_bounds.height), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + state.icon.update(icon_text); + + let icon_width = state.icon.min_width(); + + let (text_position, icon_position) = match icon.side { + Side::Left => ( + Point::new( + padding.left + icon_width + icon.spacing, + padding.top, + ), + Point::new(padding.left, padding.top), + ), + Side::Right => ( + Point::new(padding.left, padding.top), + Point::new( + padding.left + text_bounds.width - icon_width, + padding.top, + ), + ), + }; + + let text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ) + .move_to(text_position); + + let icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)) + .move_to(icon_position); + + layout::Node::with_children( + text_bounds.expand(padding), + vec![text_node, icon_node], + ) + } else { + let text = layout::Node::new(text_bounds) + .move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(text_bounds.expand(padding), vec![text]) + } } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -243,19 +313,173 @@ where value: Option<&Value>, viewport: &Rectangle, ) { - draw( - renderer, - theme, - layout, - cursor, - tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - value.unwrap_or(&self.value), - self.on_input.is_none(), - self.is_secure, - self.icon.as_ref(), - &self.style, - viewport, + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let value = value.unwrap_or(&self.value); + let is_disabled = self.on_input.is_none(); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let bounds = layout.bounds(); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + let is_mouse_over = cursor.is_over(bounds); + + let status = if is_disabled { + Status::Disabled + } else if state.is_focused() { + Status::Focused + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }; + + let appearance = (self.style)(theme, status); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.border, + ..renderer::Quad::default() + }, + appearance.background, ); + + if self.icon.is_some() { + let icon_layout = children_layout.next().unwrap(); + + renderer.fill_paragraph( + &state.icon, + icon_layout.bounds().center(), + appearance.icon, + *viewport, + ); + } + + let text = value.to_string(); + + let (cursor, offset) = if let Some(focus) = state + .is_focused + .as_ref() + .filter(|focus| focus.is_window_focused) + { + match state.cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + position, + ); + + 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, + }, + ..renderer::Quad::default() + }, + appearance.value, + )) + } 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( + &state.value, + text_bounds, + left, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + right, + ); + + 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, + }, + ..renderer::Quad::default() + }, + appearance.selection, + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let draw = |renderer: &mut Renderer, viewport| { + if let Some((cursor, color)) = cursor { + renderer.with_translation( + Vector::new(-offset, 0.0), + |renderer| { + renderer.fill_quad(cursor, color); + }, + ); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); + } + + renderer.fill_paragraph( + if text.is_empty() { + &state.placeholder + } else { + &state.value + }, + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), + if text.is_empty() { + appearance.placeholder + } else { + appearance.value + }, + viewport, + ); + }; + + if cursor.is_some() { + renderer + .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); + } else { + draw(renderer, text_bounds); + } } } @@ -263,7 +487,6 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -299,20 +522,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.font, - self.line_height, - self.icon.as_ref(), - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - &self.value, - &self.placeholder, - self.is_secure, - ) + self.layout(tree, renderer, limits, None) } fn operate( @@ -339,23 +549,468 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - renderer, - clipboard, - shell, - &mut self.value, - self.size, - self.line_height, - self.font, - self.is_secure, - self.on_input.as_deref(), - self.on_paste.as_deref(), - &self.on_submit, - || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - ) + let update_cache = |state, value| { + replace_paragraph( + renderer, + state, + layout, + value, + self.font, + self.size, + self.line_height, + ); + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state::<Renderer>(tree); + + let click_position = if self.on_input.is_some() { + cursor.position_over(layout.bounds()) + } else { + None + }; + + state.is_focused = if click_position.is_some() { + state.is_focused.or_else(|| { + let now = Instant::now(); + + Some(Focus { + updated_at: now, + now, + is_window_focused: true, + }) + }) + } else { + None + }; + + if let Some(cursor_position) = click_position { + 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 self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + find_cursor_position( + text_layout.bounds(), + &value, + state, + target, + ) + } else { + None + } + .unwrap_or(0); + + if state.keyboard_modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + position, + ); + } else { + state.cursor.move_to(position); + } + state.is_dragging = true; + } + click::Kind::Double => { + if self.is_secure { + state.cursor.select_all(&self.value); + } else { + let position = find_cursor_position( + text_layout.bounds(), + &self.value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + self.value.previous_start_of_word(position), + self.value.next_end_of_word(position), + ); + } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(&self.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::<Renderer>(tree).is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state::<Renderer>(tree); + + if state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; + + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + let position = find_cursor_position( + text_layout.bounds(), + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { + key, text, .. + }) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + + let modifiers = state.keyboard_modifiers; + focus.updated_at = Instant::now(); + + match key.as_ref() { + keyboard::Key::Character("c") + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(&self.value) + { + clipboard.write( + clipboard::Kind::Standard, + self.value.select(start, end).to_string(), + ); + } + + return event::Status::Captured; + } + keyboard::Key::Character("x") + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(&self.value) + { + clipboard.write( + clipboard::Kind::Standard, + self.value.select(start, end).to_string(), + ); + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + keyboard::Key::Character("v") + if state.keyboard_modifiers.command() + && !state.keyboard_modifiers.alt() => + { + let content = match state.is_pasting.take() { + Some(content) => content, + None => { + let content: String = clipboard + .read(clipboard::Kind::Standard) + .unwrap_or_default() + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.paste(content.clone()); + + let message = if let Some(paste) = &self.on_paste { + (paste)(editor.contents()) + } else { + (on_input)(editor.contents()) + }; + shell.publish(message); + + state.is_pasting = Some(content); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + keyboard::Key::Character("a") + if state.keyboard_modifiers.command() => + { + state.cursor.select_all(&self.value); + + return event::Status::Captured; + } + _ => {} + } + + if let Some(text) = text { + state.is_pasting = None; + + if let Some(c) = + text.chars().next().filter(|c| !c.is_control()) + { + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { + if let Some(on_submit) = self.on_submit.clone() { + shell.publish(on_submit); + } + } + keyboard::Key::Named(key::Named::Backspace) => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(&self.value).is_none() + { + if self.is_secure { + let cursor_pos = + state.cursor.end(&self.value); + state.cursor.select_range(0, cursor_pos); + } else { + state + .cursor + .select_left_by_words(&self.value); + } + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.backspace(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + } + keyboard::Key::Named(key::Named::Delete) => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(&self.value).is_none() + { + if self.is_secure { + let cursor_pos = + state.cursor.end(&self.value); + state.cursor.select_range( + cursor_pos, + self.value.len(), + ); + } else { + state + .cursor + .select_right_by_words(&self.value); + } + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + } + keyboard::Key::Named(key::Named::ArrowLeft) => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + state + .cursor + .select_left_by_words(&self.value); + } else { + state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift() { + state.cursor.select_left(&self.value); + } else { + state.cursor.move_left(&self.value); + } + } + keyboard::Key::Named(key::Named::ArrowRight) => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + state + .cursor + .select_right_by_words(&self.value); + } else { + state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift() { + state.cursor.select_right(&self.value); + } else { + state.cursor.move_right(&self.value); + } + } + keyboard::Key::Named(key::Named::Home) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::End) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } + keyboard::Key::Named(key::Named::Escape) => { + state.is_focused = None; + state.is_dragging = false; + state.is_pasting = None; + + state.keyboard_modifiers = + keyboard::Modifiers::default(); + } + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { + let state = state::<Renderer>(tree); + + if state.is_focused.is_some() { + match key.as_ref() { + keyboard::Key::Character("v") => { + state.is_pasting = None; + } + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + + state.is_pasting = None; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state::<Renderer>(tree); + + state.keyboard_modifiers = modifiers; + } + Event::Window(_, window::Event::Unfocused) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + focus.is_window_focused = false; + } + } + Event::Window(_, window::Event::Focused) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + focus.is_window_focused = true; + focus.updated_at = Instant::now(); + + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Window(_, window::Event::RedrawRequested(now)) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + if focus.is_window_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 } fn draw( @@ -368,19 +1023,7 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - draw( - renderer, - theme, - layout, - cursor, - tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - &self.value, - self.on_input.is_none(), - self.is_secure, - self.icon.as_ref(), - &self.style, - viewport, - ); + self.draw(tree, renderer, theme, layout, cursor, None, viewport); } fn mouse_interaction( @@ -391,7 +1034,15 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, self.on_input.is_none()) + if cursor.is_over(layout.bounds()) { + if self.on_input.is_none() { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } } } @@ -399,7 +1050,7 @@ impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -488,767 +1139,6 @@ 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<Pixels>, - font: Option<Renderer::Font>, - line_height: text::LineHeight, - icon: Option<&Icon<Renderer::Font>>, - state: &mut State<Renderer::Paragraph>, - value: &Value, - placeholder: &str, - is_secure: bool, -) -> layout::Node -where - Renderer: text::Renderer, -{ - let font = font.unwrap_or_else(|| renderer.default_font()); - let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let height = line_height.to_absolute(text_size); - - let limits = limits.width(width).shrink(padding); - let text_bounds = limits.resolve(width, height, Size::ZERO); - - let placeholder_text = Text { - font, - line_height, - content: placeholder, - bounds: Size::new(f32::INFINITY, text_bounds.height), - size: text_size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }; - - state.placeholder.update(placeholder_text); - - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - state.value.update(Text { - content: &value.to_string(), - ..placeholder_text - }); - - if let Some(icon) = icon { - let icon_text = Text { - line_height, - content: &icon.code_point.to_string(), - font: icon.font, - size: icon.size.unwrap_or_else(|| renderer.default_size()), - bounds: Size::new(f32::INFINITY, text_bounds.height), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }; - - state.icon.update(icon_text); - - let icon_width = state.icon.min_width(); - - let (text_position, icon_position) = match icon.side { - Side::Left => ( - Point::new( - padding.left + icon_width + icon.spacing, - padding.top, - ), - Point::new(padding.left, padding.top), - ), - Side::Right => ( - Point::new(padding.left, padding.top), - Point::new( - padding.left + text_bounds.width - icon_width, - padding.top, - ), - ), - }; - - let text_node = layout::Node::new( - text_bounds - Size::new(icon_width + icon.spacing, 0.0), - ) - .move_to(text_position); - - let icon_node = - layout::Node::new(Size::new(icon_width, text_bounds.height)) - .move_to(icon_position); - - layout::Node::with_children( - text_bounds.expand(padding), - vec![text_node, icon_node], - ) - } else { - let text = layout::Node::new(text_bounds) - .move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(text_bounds.expand(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: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - value: &mut Value, - size: Option<Pixels>, - line_height: text::LineHeight, - font: Option<Renderer::Font>, - is_secure: bool, - on_input: Option<&dyn Fn(String) -> Message>, - on_paste: Option<&dyn Fn(String) -> Message>, - on_submit: &Option<Message>, - state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>, -) -> event::Status -where - Message: Clone, - Renderer: text::Renderer, -{ - let update_cache = |state, value| { - replace_paragraph( - renderer, - state, - layout, - value, - font, - size, - line_height, - ); - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - let click_position = if on_input.is_some() { - cursor.position_over(layout.bounds()) - } else { - None - }; - - state.is_focused = if click_position.is_some() { - state.is_focused.or_else(|| { - let now = Instant::now(); - - Some(Focus { - updated_at: now, - now, - is_window_focused: true, - }) - }) - } else { - None - }; - - if let Some(cursor_position) = click_position { - 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( - text_layout.bounds(), - &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( - text_layout.bounds(), - 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( - text_layout.bounds(), - &value, - state, - target, - ) - .unwrap_or(0); - - state - .cursor - .select_range(state.cursor.start(&value), position); - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { - return event::Status::Ignored; - }; - - let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); - - match key.as_ref() { - keyboard::Key::Character("c") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard.write( - clipboard::Kind::Standard, - value.select(start, end).to_string(), - ); - } - - return event::Status::Captured; - } - keyboard::Key::Character("x") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard.write( - clipboard::Kind::Standard, - value.select(start, end).to_string(), - ); - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - - return event::Status::Captured; - } - keyboard::Key::Character("v") - if state.keyboard_modifiers.command() - && !state.keyboard_modifiers.alt() => - { - let content = match state.is_pasting.take() { - Some(content) => content, - None => { - let content: String = clipboard - .read(clipboard::Kind::Standard) - .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_input)(editor.contents()) - }; - shell.publish(message); - - state.is_pasting = Some(content); - - update_cache(state, value); - - return event::Status::Captured; - } - keyboard::Key::Character("a") - if state.keyboard_modifiers.command() => - { - state.cursor.select_all(value); - - return event::Status::Captured; - } - _ => {} - } - - if let Some(text) = text { - state.is_pasting = None; - - if let Some(c) = - text.chars().next().filter(|c| !c.is_control()) - { - let mut editor = Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - update_cache(state, value); - - return event::Status::Captured; - } - } - - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - if let Some(on_submit) = on_submit.clone() { - shell.publish(on_submit); - } - } - keyboard::Key::Named(key::Named::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_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Named(key::Named::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_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Named(key::Named::ArrowLeft) => { - 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::Key::Named(key::Named::ArrowRight) => { - 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::Key::Named(key::Named::Home) => { - if modifiers.shift() { - state - .cursor - .select_range(state.cursor.start(value), 0); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(key::Named::End) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - value.len(), - ); - } else { - state.cursor.move_to(value.len()); - } - } - keyboard::Key::Named(key::Named::Escape) => { - state.is_focused = None; - state.is_dragging = false; - state.is_pasting = None; - - state.keyboard_modifiers = - keyboard::Modifiers::default(); - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { - let state = state(); - - if state.is_focused.is_some() { - match key.as_ref() { - keyboard::Key::Character("v") => { - state.is_pasting = None; - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } - - state.is_pasting = None; - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - } - Event::Window(_, window::Event::Unfocused) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.is_window_focused = false; - } - } - Event::Window(_, window::Event::Focused) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.is_window_focused = true; - focus.updated_at = Instant::now(); - - shell.request_redraw(window::RedrawRequest::NextFrame); - } - } - Event::Window(_, window::Event::RedrawRequested(now)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - if focus.is_window_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<Theme, Renderer>( - renderer: &mut Renderer, - theme: &Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State<Renderer::Paragraph>, - value: &Value, - is_disabled: bool, - is_secure: bool, - icon: Option<&Icon<Renderer::Font>>, - style: &Theme::Style, - viewport: &Rectangle, -) where - Theme: StyleSheet, - Renderer: text::Renderer, -{ - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - let bounds = layout.bounds(); - - let mut children_layout = layout.children(); - let text_bounds = children_layout.next().unwrap().bounds(); - - let is_mouse_over = cursor.is_over(bounds); - - let appearance = if is_disabled { - theme.disabled(style) - } else 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: appearance.border, - ..renderer::Quad::default() - }, - appearance.background, - ); - - if icon.is_some() { - let icon_layout = children_layout.next().unwrap(); - - renderer.fill_paragraph( - &state.icon, - icon_layout.bounds().center(), - appearance.icon_color, - *viewport, - ); - } - - let text = value.to_string(); - - let (cursor, offset) = if let Some(focus) = state - .is_focused - .as_ref() - .filter(|focus| focus.is_window_focused) - { - match state.cursor.state(value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - position, - ); - - 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, - }, - ..renderer::Quad::default() - }, - 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( - &state.value, - text_bounds, - left, - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - right, - ); - - 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, - }, - ..renderer::Quad::default() - }, - theme.selection_color(style), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let draw = |renderer: &mut Renderer, viewport| { - if let Some((cursor, color)) = cursor { - renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { - renderer.fill_quad(cursor, color); - }); - } else { - renderer.with_translation(Vector::ZERO, |_| {}); - } - - renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, - Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), - if text.is_empty() { - theme.placeholder_color(style) - } else if is_disabled { - theme.disabled_color(style) - } else { - theme.value_color(style) - }, - viewport, - ); - }; - - if cursor.is_some() { - renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport)); - } else { - draw(renderer, text_bounds); - } -} - -/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - is_disabled: bool, -) -> mouse::Interaction { - if cursor.is_over(layout.bounds()) { - if is_disabled { - mouse::Interaction::NotAllowed - } else { - mouse::Interaction::Text - } - } else { - mouse::Interaction::default() - } -} - /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State<P: text::Paragraph> { @@ -1264,6 +1154,12 @@ pub struct State<P: text::Paragraph> { // TODO: Add stateful horizontal scrolling offset } +fn state<Renderer: text::Renderer>( + tree: &mut Tree, +) -> &mut State<Renderer::Paragraph> { + tree.state.downcast_mut::<State<Renderer::Paragraph>>() +} + #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, @@ -1479,3 +1375,86 @@ fn replace_paragraph<Renderer>( } const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + +/// The possible status of a [`TextInput`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`TextInput`] can be interacted with. + Active, + /// The [`TextInput`] is being hovered. + Hovered, + /// The [`TextInput`] is focused. + Focused, + /// The [`TextInput`] cannot be interacted with. + Disabled, +} + +/// The appearance of a text input. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`Background`] of the text input. + pub background: Background, + /// The [`Border`] of the text input. + pub border: Border, + /// The [`Color`] of the icon of the text input. + pub icon: Color, + /// The [`Color`] of the placeholder of the text input. + pub placeholder: Color, + /// The [`Color`] of the value of the text input. + pub value: Color, + /// The [`Color`] of the selection of the text input. + pub selection: Color, +} + +/// The definiton of the default style of a [`TextInput`]. +pub trait Style { + /// Returns the default style of a [`TextInput`]. + fn style() -> fn(&Self, Status) -> Appearance; +} + +impl Style for Theme { + fn style() -> fn(&Self, Status) -> Appearance { + default + } +} + +/// The default style of a [`TextInput`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let active = Appearance { + background: Background::Color(palette.background.base.color), + border: Border { + radius: 2.0.into(), + width: 1.0, + color: palette.background.strong.color, + }, + icon: palette.background.weak.text, + placeholder: palette.background.strong.color, + value: palette.background.base.text, + selection: palette.primary.weak.color, + }; + + match status { + Status::Active => active, + Status::Hovered => Appearance { + border: Border { + color: palette.background.base.text, + ..active.border + }, + ..active + }, + Status::Focused => Appearance { + border: Border { + color: palette.primary.strong.color, + ..active.border + }, + ..active + }, + Status::Disabled => Appearance { + background: Background::Color(palette.background.weak.color), + value: active.placeholder, + ..active + }, + } +} |