diff --git a/src/theme/style/text_input.rs b/src/theme/style/text_input.rs index 1970eabe..74297c20 100644 --- a/src/theme/style/text_input.rs +++ b/src/theme/style/text_input.rs @@ -11,6 +11,7 @@ use iced_core::Color; pub enum TextInput { #[default] Default, + EditableText, ExpandableSearch, Search, Inline, @@ -52,6 +53,22 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::EditableText => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, TextInput::ExpandableSearch => Appearance { background: Color::TRANSPARENT.into(), border_radius: corner.radius_xl.into(), @@ -147,7 +164,7 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, - TextInput::Inline => Appearance { + TextInput::EditableText | TextInput::Inline => Appearance { background: Color::TRANSPARENT.into(), border_radius: corner.radius_0.into(), border_width: 0.0, @@ -226,6 +243,22 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::EditableText => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, TextInput::Inline => Appearance { background: Color::from(container.component.hover).into(), border_radius: corner.radius_0.into(), @@ -289,6 +322,24 @@ impl StyleSheet for crate::Theme { selected_fill: palette.accent_color().into(), label_color: label_color.into(), }, + TextInput::EditableText => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + // TODO use regular text color here after text rendering handles multiple colors + // in this case, for selected and unselected text + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, TextInput::Inline => Appearance { background: Color::from(palette.accent.base).into(), border_radius: corner.radius_0.into(), diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 7077f368..8831d81d 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -60,6 +60,27 @@ where TextInput::new(placeholder, value) } +/// A text label whiich can transform into a text input on activation. +pub fn editable_input<'a, Message: Clone + 'static>( + placeholder: impl Into>, + text: impl Into>, + editing: bool, + on_toggle_edit: impl Fn(bool) -> Message + 'a, +) -> TextInput<'a, Message> { + let icon = crate::widget::icon::from_name(if editing { + "edit-clear-symbolic" + } else { + "edit-symbolic" + }); + + TextInput::new(placeholder, text) + .style(crate::theme::TextInput::EditableText) + .editable() + .editing(editing) + .on_toggle_edit(on_toggle_edit) + .trailing_icon(icon.size(16).into()) +} + /// Creates a new search [`TextInput`]. /// /// [`TextInput`]: widget::TextInput @@ -161,6 +182,8 @@ pub struct TextInput<'a, Message> { placeholder: Cow<'a, str>, value: Value, is_secure: bool, + is_editable: bool, + is_read_only: bool, font: Option<::Font>, width: Length, padding: Padding, @@ -172,6 +195,7 @@ pub struct TextInput<'a, Message> { on_input: Option Message + 'a>>, on_paste: Option Message + 'a>>, on_submit: Option, + on_toggle_edit: Option Message + 'a>>, leading_icon: Option>, trailing_icon: Option>, style: ::Style, @@ -201,6 +225,8 @@ where placeholder: placeholder.into(), value: Value::new(v.as_ref()), is_secure: false, + is_editable: false, + is_read_only: false, font: None, width: Length::Fill, padding: [spacing, spacing, spacing, spacing].into(), @@ -210,6 +236,7 @@ where on_input: None, on_paste: None, on_submit: None, + on_toggle_edit: None, leading_icon: None, trailing_icon: None, error: None, @@ -260,6 +287,16 @@ where self } + fn editable(mut self) -> Self { + self.is_editable = true; + self + } + + fn editing(mut self, enable: bool) -> Self { + self.is_read_only = !enable; + self + } + /// Sets the message that should be produced when some text is typed into /// the [`TextInput`]. /// @@ -285,6 +322,14 @@ where self } + pub fn on_toggle_edit(mut self, callback: F) -> Self + where + F: 'a + Fn(bool) -> Message, + { + self.on_toggle_edit = Some(Box::new(callback)); + 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 { @@ -458,14 +503,14 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new(self.is_secure)) + tree::State::new(State::new(self.is_secure, self.is_read_only)) } fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); // Unfocus text input if it becomes disabled - if self.on_input.is_none() { + if self.on_input.is_none() || state.is_read_only { state.last_click = None; state.is_focused = None; state.is_pasting = None; @@ -502,6 +547,10 @@ where state.dirty = true; } + if state.is_read_only != self.is_read_only { + self.is_read_only = state.is_read_only; + } + let mut children: Vec<_> = self .leading_icon .iter_mut() @@ -664,68 +713,36 @@ where viewport: &Rectangle, ) -> event::Status { let text_layout = self.text_layout(layout); - let mut index = 0; + let mut trailing_icon_layout = None; let font = self.font.unwrap_or_else(|| renderer.default_font()); let size = self.size.unwrap_or_else(|| renderer.default_size().0); let line_height = self.line_height; - if let (Some(leading_icon), Some(tree)) = - (self.leading_icon.as_mut(), tree.children.get_mut(index)) - { - let mut children = text_layout.children(); - children.next(); - let leading_icon_layout = children.next().unwrap(); - - if cursor_position.is_over(leading_icon_layout.bounds()) - || matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft) - ) + if self.is_editable { + let index = tree.children.len() - 1; + if let (Some(trailing_icon), Some(tree)) = + (self.trailing_icon.as_mut(), tree.children.get_mut(index)) { - let res = leading_icon.as_widget_mut().on_event( - tree, - event.clone(), - leading_icon_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - if res == event::Status::Captured { - return res; - } - } - index += 1; - } - if let (Some(trailing_icon), Some(tree)) = - (self.trailing_icon.as_mut(), tree.children.get_mut(index)) - { - let mut children = text_layout.children(); - children.next(); - if self.leading_icon.is_some() { - children.next(); - } - let trailing_icon_layout = children.next().unwrap(); + let children = text_layout.children(); + trailing_icon_layout = Some(children.last().unwrap()); - if cursor_position.is_over(trailing_icon_layout.bounds()) - | matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft) - ) - { - let res = trailing_icon.as_widget_mut().on_event( - tree, - event.clone(), - trailing_icon_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - if res == event::Status::Captured { - return res; + if let Some(trailing_layout) = trailing_icon_layout { + if cursor_position.is_over(trailing_layout.bounds()) { + let res = trailing_icon.as_widget_mut().on_event( + tree, + event.clone(), + trailing_layout, + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); + + if res == event::Status::Captured { + return res; + } + } } } } @@ -733,6 +750,7 @@ where update( event, text_layout.children().next().unwrap(), + trailing_icon_layout, cursor_position, clipboard, shell, @@ -740,9 +758,11 @@ where size, font, self.is_secure, + self.is_editable, self.on_input.as_deref(), self.on_paste.as_deref(), &self.on_submit, + self.on_toggle_edit.as_deref(), || tree.state.downcast_mut::(), self.on_create_dnd_source.as_deref(), self.dnd_icon, @@ -1093,6 +1113,7 @@ pub fn layout( pub fn update<'a, Message>( event: Event, text_layout: Layout<'_>, + trailing_icon_layout: Option>, cursor_position: mouse::Cursor, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, @@ -1100,9 +1121,11 @@ pub fn update<'a, Message>( size: f32, font: ::Font, is_secure: bool, + is_editable: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, on_submit: &Option, + on_toggle_edit: Option<&dyn Fn(bool) -> Message>, state: impl FnOnce() -> &'a mut State, #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, #[allow(unused_variables)] dnd_icon: bool, @@ -1132,7 +1155,7 @@ where let state = state(); let is_clicked = cursor_position.is_over(text_layout.bounds()) && on_input.is_some(); - state.is_focused = if is_clicked { + state.is_focused = if is_clicked && !state.is_read_only { state.is_focused.or_else(|| { let now = Instant::now(); Some(Focus { @@ -1300,6 +1323,47 @@ where return event::Status::Captured; } + + if is_editable { + if let Some(trailing_layout) = trailing_icon_layout { + let is_trailing_clicked = cursor_position.is_over(trailing_layout.bounds()) + && on_toggle_edit.is_some(); + + if is_trailing_clicked { + let Some(pos) = cursor_position.position() else { + return event::Status::Ignored; + }; + + let click = mouse::Click::new(pos, state.last_click); + + match ( + &state.dragging_state, + click.kind(), + state.cursor().state(value), + ) { + (None, click::Kind::Single, _) => { + state.is_read_only = !state.is_read_only; + if let Some(on_toggle_edit) = on_toggle_edit { + let message = (on_toggle_edit)(!state.is_read_only); + shell.publish(message); + + let now = Instant::now(); + state.is_focused = Some(Focus { + updated_at: now, + now, + }); + + state.move_cursor_to_end(); + return event::Status::Captured; + } + } + _ => { + state.dragging_state = None; + } + } + } + } + } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { @@ -1332,6 +1396,10 @@ where return event::Status::Ignored; }; + if state.is_read_only { + return event::Status::Ignored; + } + let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -2263,6 +2331,7 @@ pub struct State { pub helper_text: crate::Paragraph, pub dirty: bool, pub is_secure: bool, + pub is_read_only: bool, is_focused: Option, dragging_state: Option, #[cfg(feature = "wayland")] @@ -2282,9 +2351,10 @@ struct Focus { impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - pub fn new(is_secure: bool) -> Self { + pub fn new(is_secure: bool, is_read_only: bool) -> Self { Self { is_secure, + is_read_only, ..Self::default() } } @@ -2314,14 +2384,14 @@ impl State { } /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused(is_secure: bool) -> Self { + pub fn focused(is_secure: bool, is_read_only: bool) -> Self { Self { is_secure, value: crate::Paragraph::new(), placeholder: crate::Paragraph::new(), label: crate::Paragraph::new(), helper_text: crate::Paragraph::new(), - + is_read_only, is_focused: None, dragging_state: None, #[cfg(feature = "wayland")]