summaryrefslogtreecommitdiffstats
path: root/widget/src
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-03-05 15:53:59 +0100
committerLibravatar Héctor Ramón Jiménez <hector@hecrj.dev>2024-03-05 15:53:59 +0100
commit704ec9cb5cdc1d44f2df2f15de700b0af330b1d7 (patch)
tree27aeef02032c47327e74c9b3e48e83e11cde7fb2 /widget/src
parentd681aaa57e3106cf0ce90b74ade040ca7bb97832 (diff)
downloadiced-704ec9cb5cdc1d44f2df2f15de700b0af330b1d7.tar.gz
iced-704ec9cb5cdc1d44f2df2f15de700b0af330b1d7.tar.bz2
iced-704ec9cb5cdc1d44f2df2f15de700b0af330b1d7.zip
Simplify theming for `TextInput` widget
Diffstat (limited to 'widget/src')
-rw-r--r--widget/src/combo_box.rs27
-rw-r--r--widget/src/helpers.rs8
-rw-r--r--widget/src/overlay/menu.rs4
-rw-r--r--widget/src/pick_list.rs14
-rw-r--r--widget/src/scrollable.rs18
-rw-r--r--widget/src/text_input.rs1669
6 files changed, 855 insertions, 885 deletions
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
+ },
+ }
+}