use std::borrow::Cow;
use std::cell::{Cell, LazyCell};
use crate::ext::ColorExt;
use crate::theme::THEME;
use super::cursor;
pub use super::cursor::Cursor;
use super::editor::Editor;
use super::style::StyleSheet;
pub use super::value::Value;
use apply::Apply;
use cosmic_theme::Theme;
use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent};
use iced::clipboard::mime::AsMimeTypes;
use iced::Limits;
use iced_core::event::{self, Event};
use iced_core::mouse::{self, click};
use iced_core::overlay::Group;
use iced_core::renderer::{self, Renderer as CoreRenderer};
use iced_core::text::{self, Paragraph, Renderer, Text};
use iced_core::time::{Duration, Instant};
use iced_core::touch;
use iced_core::widget::operation::{self, Operation};
use iced_core::widget::tree::{self, Tree};
use iced_core::widget::Id;
use iced_core::window;
use iced_core::{alignment, Background};
use iced_core::{keyboard, Border, Shadow};
use iced_core::{layout, overlay};
use iced_core::{
Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size,
Vector, Widget,
};
#[cfg(feature = "wayland")]
use iced_renderer::core::event::{wayland, PlatformSpecific};
#[cfg(feature = "wayland")]
use iced_runtime::platform_specific;
use iced_runtime::{task, Action, Task};
thread_local! {
static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now()));
}
pub fn text_input<'a, Message>(
placeholder: impl Into<Cow<'a, str>>,
value: impl Into<Cow<'a, str>>,
) -> TextInput<'a, Message>
where
Message: Clone + 'static,
{
TextInput::new(placeholder, value)
}
pub fn editable_input<'a, Message: Clone + 'static>(
placeholder: impl Into<Cow<'a, str>>,
text: impl Into<Cow<'a, str>>,
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())
}
pub fn search_input<'a, Message>(
placeholder: impl Into<Cow<'a, str>>,
value: impl Into<Cow<'a, str>>,
) -> TextInput<'a, Message>
where
Message: Clone + 'static,
{
let spacing = THEME.lock().unwrap().cosmic().space_xxs();
TextInput::new(placeholder, value)
.padding([0, spacing])
.style(crate::theme::TextInput::Search)
.leading_icon(
crate::widget::icon::from_name("system-search-symbolic")
.size(16)
.apply(crate::widget::container)
.padding(8)
.into(),
)
}
pub fn secure_input<'a, Message>(
placeholder: impl Into<Cow<'a, str>>,
value: impl Into<Cow<'a, str>>,
on_visible_toggle: Option<Message>,
hidden: bool,
) -> TextInput<'a, Message>
where
Message: Clone + 'static,
{
let spacing = THEME.lock().unwrap().cosmic().space_xxs();
let mut input = TextInput::new(placeholder, value)
.padding([0, spacing])
.style(crate::theme::TextInput::Default)
.leading_icon(
crate::widget::icon::from_name("system-lock-screen-symbolic")
.size(16)
.apply(crate::widget::container)
.padding(8)
.into(),
);
if hidden {
input = input.password();
}
if let Some(msg) = on_visible_toggle {
input.trailing_icon(
crate::widget::icon::from_name(if hidden {
"document-properties-symbolic"
} else {
"image-red-eye-symbolic"
})
.size(16)
.apply(crate::widget::button::custom)
.class(crate::theme::Button::Icon)
.on_press(msg)
.padding(8)
.into(),
)
} else {
input
}
}
pub fn inline_input<'a, Message>(
placeholder: impl Into<Cow<'a, str>>,
value: impl Into<Cow<'a, str>>,
) -> TextInput<'a, Message>
where
Message: Clone + 'static,
{
let spacing = THEME.lock().unwrap().cosmic().space_xxs();
TextInput::new(placeholder, value)
.style(crate::theme::TextInput::Inline)
.padding(spacing)
}
pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[
"text/plain;charset=utf-8",
"text/plain;charset=UTF-8",
"UTF8_STRING",
"STRING",
"text/plain",
"TEXT",
];
#[allow(missing_debug_implementations)]
#[must_use]
pub struct TextInput<'a, Message> {
id: Id,
placeholder: Cow<'a, str>,
value: Value,
is_secure: bool,
is_editable: bool,
is_read_only: bool,
select_on_focus: bool,
font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
width: Length,
padding: Padding,
size: Option<f32>,
helper_size: f32,
label: Option<&'a str>,
helper_text: Option<&'a str>,
error: Option<&'a str>,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
on_submit: Option<Message>,
on_toggle_edit: Option<Box<dyn Fn(bool) -> Message + 'a>>,
leading_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
trailing_icon: Option<Element<'a, Message, crate::Theme, crate::Renderer>>,
style: <crate::Theme as StyleSheet>::Style,
on_create_dnd_source: Option<Box<dyn Fn(State) -> Message + 'a>>,
surface_ids: Option<(window::Id, window::Id)>,
dnd_icon: bool,
line_height: text::LineHeight,
helper_line_height: text::LineHeight,
always_active: bool,
}
impl<'a, Message> TextInput<'a, Message>
where
Message: Clone + 'static,
{
pub fn new(placeholder: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
let spacing = THEME.lock().unwrap().cosmic().space_xxs();
let v: Cow<'a, str> = value.into();
TextInput {
id: Id::unique(),
placeholder: placeholder.into(),
value: Value::new(v.as_ref()),
is_secure: false,
is_editable: false,
is_read_only: false,
select_on_focus: false,
font: None,
width: Length::Fill,
padding: spacing.into(),
size: None,
helper_size: 10.0,
helper_line_height: text::LineHeight::Absolute(14.0.into()),
on_input: None,
on_paste: None,
on_submit: None,
on_toggle_edit: None,
leading_icon: None,
trailing_icon: None,
error: None,
style: crate::theme::TextInput::default(),
on_create_dnd_source: None,
surface_ids: None,
dnd_icon: false,
line_height: text::LineHeight::default(),
label: None,
helper_text: None,
always_active: false,
}
}
fn dnd_id(&self) -> u128 {
match &self.id.0 {
iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => {
*id as u128
}
_ => unreachable!(),
}
}
pub fn always_active(mut self) -> Self {
self.always_active = true;
self
}
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
pub fn helper_text(mut self, helper_text: &'a str) -> Self {
self.helper_text = Some(helper_text);
self
}
pub fn id(mut self, id: Id) -> Self {
self.id = id;
self
}
pub fn error(mut self, error: &'a str) -> Self {
self.error = Some(error);
self
}
pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
self.line_height = line_height.into();
self
}
pub fn password(mut self) -> Self {
self.is_secure = true;
self
}
pub fn editable(mut self) -> Self {
self.is_editable = true;
self
}
pub fn editing(mut self, enable: bool) -> Self {
self.is_read_only = !enable;
self
}
pub fn select_on_focus(mut self, select_on_focus: bool) -> Self {
self.select_on_focus = select_on_focus;
self
}
pub fn on_input<F>(mut self, callback: F) -> Self
where
F: 'a + Fn(String) -> Message,
{
self.on_input = Some(Box::new(callback));
self
}
pub fn on_submit(self, message: Message) -> Self {
self.on_submit_maybe(Some(message))
}
pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self {
self.on_submit = message;
self
}
pub fn on_toggle_edit<F>(mut self, callback: F) -> Self
where
F: 'a + Fn(bool) -> Message,
{
self.on_toggle_edit = Some(Box::new(callback));
self
}
pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self {
self.on_paste = Some(Box::new(on_paste));
self
}
pub fn font(mut self, font: <crate::Renderer as iced_core::text::Renderer>::Font) -> Self {
self.font = Some(font);
self
}
pub fn leading_icon(
mut self,
icon: Element<'a, Message, crate::Theme, crate::Renderer>,
) -> Self {
self.leading_icon = Some(icon);
self
}
pub fn trailing_icon(
mut self,
icon: Element<'a, Message, crate::Theme, crate::Renderer>,
) -> Self {
self.trailing_icon = Some(icon);
self
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
self.padding = padding.into();
self
}
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
self.size = Some(size.into().0);
self
}
pub fn style(mut self, style: impl Into<<crate::Theme as StyleSheet>::Style>) -> Self {
self.style = style.into();
self
}
#[allow(clippy::too_many_arguments)]
pub fn draw(
&self,
tree: &Tree,
renderer: &mut crate::Renderer,
theme: &crate::Theme,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
value: Option<&Value>,
style: &renderer::Style,
) {
let text_layout = self.text_layout(layout);
draw(
renderer,
theme,
layout,
text_layout,
cursor_position,
tree,
value.unwrap_or(&self.value),
&self.placeholder,
self.size,
self.font,
self.on_input.is_none(),
self.is_secure,
self.leading_icon.as_ref(),
self.trailing_icon.as_ref(),
&self.style,
self.dnd_icon,
self.line_height,
self.error,
self.label,
self.helper_text,
self.helper_size,
self.helper_line_height,
&layout.bounds(),
style,
);
}
#[cfg(feature = "wayland")]
pub fn on_start_dnd(mut self, on_start_dnd: impl Fn(State) -> Message + 'a) -> Self {
self.on_create_dnd_source = Some(Box::new(on_start_dnd));
self
}
pub fn surface_ids(mut self, window_id: (window::Id, window::Id)) -> Self {
self.surface_ids = Some(window_id);
self
}
pub fn dnd_icon(mut self, dnd_icon: bool) -> Self {
self.dnd_icon = dnd_icon;
self
}
pub fn on_clear(self, on_clear: Message) -> Self {
self.trailing_icon(
crate::widget::icon::from_name("edit-clear-symbolic")
.size(16)
.apply(crate::widget::button::custom)
.class(crate::theme::Button::Icon)
.on_press(on_clear)
.padding(8)
.into(),
)
}
fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> {
if self.dnd_icon {
layout
} else if self.label.is_some() {
let mut nodes = layout.children();
nodes.next();
nodes.next().unwrap()
} else {
layout.children().next().unwrap()
}
}
}
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'a, Message>
where
Message: Clone + 'static,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new(
self.is_secure,
self.is_read_only,
self.always_active,
self.select_on_focus,
))
}
fn diff(&mut self, tree: &mut Tree) {
let state = tree.state.downcast_mut::<State>();
if self.on_input.is_none() {
state.last_click = None;
state.is_focused = None;
state.is_pasting = None;
state.dragging_state = None;
}
let old_value = state
.value
.raw()
.buffer()
.lines
.iter()
.map(|l| l.text())
.collect::<String>();
if state.is_secure != self.is_secure
|| old_value != self.value.to_string()
|| state
.label
.raw()
.buffer()
.lines
.iter()
.map(|l| l.text())
.collect::<String>()
!= self.label.unwrap_or_default()
|| state
.helper_text
.raw()
.buffer()
.lines
.iter()
.map(|l| l.text())
.collect::<String>()
!= self.helper_text.unwrap_or_default()
{
state.is_secure = self.is_secure;
state.dirty = true;
}
self.is_read_only = state.is_read_only;
if self.always_active && state.is_focused.is_none() {
let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
state.is_focused = Some(Focus {
updated_at: now,
now,
});
}
let old_value = Value::new(&old_value);
if state.is_focused.is_some() {
match state.cursor.state(&old_value) {
cursor::State::Index(index) => {
if index == old_value.len() {
state.cursor.move_to(self.value.len());
}
}
_ => {}
};
}
if !state.is_focused.as_ref().map_or(false, |f| {
f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get())
}) {
state.is_focused = None;
}
let mut children: Vec<_> = self
.leading_icon
.iter_mut()
.chain(self.trailing_icon.iter_mut())
.map(iced_core::Element::as_widget_mut)
.collect();
tree.diff_children(children.as_mut_slice());
}
fn children(&self) -> Vec<Tree> {
self.leading_icon
.iter()
.chain(self.trailing_icon.iter())
.map(|icon| Tree::new(icon))
.collect()
}
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: Length::Shrink,
}
}
fn layout(
&self,
tree: &mut Tree,
renderer: &crate::Renderer,
limits: &layout::Limits,
) -> layout::Node {
let font = self.font.unwrap_or_else(|| renderer.default_font());
if self.dnd_icon {
let state = tree.state.downcast_mut::<State>();
let limits = limits.width(Length::Shrink).height(Length::Shrink);
let size = self.size.unwrap_or_else(|| renderer.default_size().0);
let bounds = limits.max();
let value_paragraph = &mut state.value;
let v = self.value.to_string();
value_paragraph.update(Text {
content: if self.value.is_empty() {
self.placeholder.as_ref()
} else {
&v
},
font,
bounds,
size: iced::Pixels(size),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
let Size { width, height } =
limits.resolve(Length::Shrink, Length::Shrink, value_paragraph.min_bounds());
let size = limits.resolve(width, height, Size::new(width, height));
layout::Node::with_children(size, vec![layout::Node::new(size)])
} else {
let res = layout(
renderer,
limits,
self.width,
self.padding,
self.size,
self.leading_icon.as_ref(),
self.trailing_icon.as_ref(),
self.line_height,
self.label,
self.helper_text,
self.helper_size,
self.helper_line_height,
font,
tree,
);
let size = self.size.unwrap_or_else(|| renderer.default_size().0);
let line_height = self.line_height;
let state = tree.state.downcast_mut::<State>();
if state.dirty {
state.dirty = false;
let value = if self.is_secure {
self.value.secure()
} else {
self.value.clone()
};
replace_paragraph(
state,
Layout::new(&res),
&value,
font,
iced::Pixels(size),
line_height,
);
}
res
}
}
fn operate(
&self,
tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &crate::Renderer,
operation: &mut dyn Operation<()>,
) {
let state = tree.state.downcast_mut::<State>();
operation.custom(state, Some(&self.id));
operation.focusable(state, Some(&self.id));
operation.text_input(state, Some(&self.id));
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
let mut layout_ = Vec::with_capacity(2);
if self.leading_icon.is_some() {
let mut children = self.text_layout(layout).children();
children.next();
layout_.push(children.next().unwrap());
}
if self.trailing_icon.is_some() {
let mut children = self.text_layout(layout).children();
children.next();
if self.leading_icon.is_some() {
children.next();
}
layout_.push(children.next().unwrap());
};
let children = self
.leading_icon
.iter_mut()
.chain(self.trailing_icon.iter_mut())
.zip(&mut tree.children)
.zip(layout_)
.filter_map(|((child, state), layout)| {
child
.as_widget_mut()
.overlay(state, layout, renderer, translation)
})
.collect::<Vec<_>>();
(!children.is_empty()).then(|| Group::with_children(children).overlay())
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
let text_layout = self.text_layout(layout);
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 self.is_editable {
if let Some(ref on_edit) = self.on_toggle_edit {
let state = tree.state.downcast_mut::<State>();
if !state.is_read_only && state.is_focused.is_none() {
state.is_read_only = true;
shell.publish((on_edit)(false));
}
}
}
if tree.children.len() > 0 {
let index = tree.children.len() - 1;
if let (Some(trailing_icon), Some(tree)) =
(self.trailing_icon.as_mut(), tree.children.get_mut(index))
{
let children = text_layout.children();
trailing_icon_layout = Some(children.last().unwrap());
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;
}
}
}
}
}
let dnd_id = self.dnd_id();
let id = Widget::id(self);
update(
id,
event,
text_layout.children().next().unwrap(),
trailing_icon_layout,
cursor_position,
clipboard,
shell,
&mut self.value,
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::<State>(),
self.on_create_dnd_source.as_deref(),
dnd_id,
line_height,
layout,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut crate::Renderer,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let text_layout = self.text_layout(layout);
draw(
renderer,
theme,
layout,
text_layout,
cursor_position,
tree,
&self.value,
&self.placeholder,
self.size,
self.font,
self.on_input.is_none(),
self.is_secure,
self.leading_icon.as_ref(),
self.trailing_icon.as_ref(),
&self.style,
self.dnd_icon,
self.line_height,
self.error,
self.label,
self.helper_text,
self.helper_size,
self.helper_line_height,
viewport,
style,
);
}
fn mouse_interaction(
&self,
state: &Tree,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
renderer: &crate::Renderer,
) -> mouse::Interaction {
let layout = self.text_layout(layout);
let mut index = 0;
if let (Some(leading_icon), Some(tree)) =
(self.leading_icon.as_ref(), state.children.get(index))
{
let mut children = layout.children();
children.next();
let leading_icon_layout = children.next().unwrap();
if cursor_position.is_over(leading_icon_layout.bounds()) {
return leading_icon.as_widget().mouse_interaction(
tree,
layout,
cursor_position,
viewport,
renderer,
);
}
index += 1;
}
if let (Some(trailing_icon), Some(tree)) =
(self.trailing_icon.as_ref(), state.children.get(index))
{
let mut children = layout.children();
children.next();
if self.leading_icon.is_some() {
children.next();
}
let trailing_icon_layout = children.next().unwrap();
if cursor_position.is_over(trailing_icon_layout.bounds()) {
return trailing_icon.as_widget().mouse_interaction(
tree,
layout,
cursor_position,
viewport,
renderer,
);
}
}
let mut children = layout.children();
let layout = children.next().unwrap();
mouse_interaction(layout, cursor_position, self.on_input.is_none())
}
fn id(&self) -> Option<Id> {
Some(self.id.clone())
}
fn set_id(&mut self, id: Id) {
self.id = id;
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles,
) {
if let Some(input) = layout.children().last() {
let Rectangle {
x,
y,
width,
height,
} = input.bounds();
dnd_rectangles.push(iced::clipboard::dnd::DndDestinationRectangle {
id: self.dnd_id(),
rectangle: iced::clipboard::dnd::Rectangle {
x: x as f64,
y: y as f64,
width: width as f64,
height: height as f64,
},
mime_types: SUPPORTED_TEXT_MIME_TYPES
.iter()
.map(|s| Cow::Borrowed(*s))
.collect(),
actions: DndAction::Move,
preferred: DndAction::Move,
});
}
}
}
impl<'a, Message> From<TextInput<'a, Message>>
for Element<'a, Message, crate::Theme, crate::Renderer>
where
Message: 'static + Clone,
{
fn from(
text_input: TextInput<'a, Message>,
) -> Element<'a, Message, crate::Theme, crate::Renderer> {
Element::new(text_input)
}
}
pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::focusable::focus(id)))
}
pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to_end(
id,
)))
}
pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to_front(
id,
)))
}
pub fn move_cursor_to<Message: 'static>(id: Id, position: usize) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to(
id, position,
)))
}
pub fn select_all<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::select_all(id)))
}
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
pub fn layout<Message>(
renderer: &crate::Renderer,
limits: &layout::Limits,
width: Length,
padding: Padding,
size: Option<f32>,
leading_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>,
trailing_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>,
line_height: text::LineHeight,
label: Option<&str>,
helper_text: Option<&str>,
helper_text_size: f32,
helper_text_line_height: text::LineHeight,
font: iced_core::Font,
tree: &mut Tree,
) -> layout::Node {
let limits = limits.width(width);
let spacing = THEME.lock().unwrap().cosmic().space_xxs();
let mut nodes = Vec::with_capacity(3);
let text_pos = if let Some(label) = label {
let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY);
let state = tree.state.downcast_mut::<State>();
let label_paragraph = &mut state.label;
label_paragraph.update(Text {
content: label,
font,
bounds: text_bounds,
size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
line_height,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
let label_size = label_paragraph.min_bounds();
nodes.push(layout::Node::new(label_size));
Vector::new(0.0, label_size.height + f32::from(spacing))
} else {
Vector::ZERO
};
let text_size = size.unwrap_or_else(|| renderer.default_size().0);
let mut text_input_height = line_height.to_absolute(text_size.into()).0;
let padding = padding.fit(Size::ZERO, limits.max());
let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() {
let children = &mut tree.children;
let limits_copy = limits;
let limits = limits.shrink(padding);
let icon_spacing = 8.0;
let mut c_i = 0;
let (leading_icon_width, mut leading_icon) =
if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) {
let size = icon.as_widget().size();
let icon_node = icon.as_widget().layout(
tree,
renderer,
&Limits::NONE.width(size.width).height(size.height),
);
text_input_height = text_input_height.max(icon_node.bounds().height);
c_i += 1;
(icon_node.bounds().width + icon_spacing, Some(icon_node))
} else {
(0.0, None)
};
let (trailing_icon_width, mut trailing_icon) =
if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) {
let size = icon.as_widget().size();
let icon_node = icon.as_widget().layout(
tree,
renderer,
&Limits::NONE.width(size.width).height(size.height),
);
text_input_height = text_input_height.max(icon_node.bounds().height);
(icon_node.bounds().width + icon_spacing, Some(icon_node))
} else {
(0.0, None)
};
let text_limits = limits
.width(width)
.height(line_height.to_absolute(text_size.into()));
let text_bounds = text_limits.resolve(width, Length::Shrink, Size::INFINITY);
let text_node = layout::Node::new(
text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0),
)
.move_to(Point::new(
padding.left + leading_icon_width,
padding.top
+ ((text_input_height - line_height.to_absolute(text_size.into()).0) / 2.0)
.max(0.0),
));
let mut node_list: Vec<_> = Vec::with_capacity(3);
let text_node_bounds = text_node.bounds();
node_list.push(text_node);
if let Some(leading_icon) = leading_icon.take() {
node_list.push(leading_icon.clone().move_to(Point::new(
padding.left,
padding.top + ((text_input_height - leading_icon.bounds().height) / 2.0).max(0.0),
)));
}
if let Some(trailing_icon) = trailing_icon.take() {
let trailing_icon = trailing_icon.clone().move_to(Point::new(
text_node_bounds.x + text_node_bounds.width + f32::from(spacing),
padding.top + ((text_input_height - trailing_icon.bounds().height) / 2.0).max(0.0),
));
node_list.push(trailing_icon);
}
let text_input_size = Size::new(
text_node_bounds.x + text_node_bounds.width + trailing_icon_width,
text_input_height,
)
.expand(padding);
let input_limits = limits_copy
.width(width)
.height(text_input_height.max(text_input_size.height))
.min_width(text_input_size.width);
let input_bounds = input_limits.resolve(
width,
text_input_height.max(text_input_size.height),
text_input_size,
);
let input_node = layout::Node::with_children(input_bounds, node_list).translate(text_pos);
let y_pos = input_node.bounds().y + input_node.bounds().height + f32::from(spacing);
nodes.push(input_node);
Vector::new(0.0, y_pos)
} else {
let limits = limits
.width(width)
.height(text_input_height + padding.vertical())
.shrink(padding);
let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY);
let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top));
let node = layout::Node::with_children(text_bounds.expand(padding), vec![text])
.translate(text_pos);
let y_pos = node.bounds().y + node.bounds().height + f32::from(spacing);
nodes.push(node);
Vector::new(0.0, y_pos)
};
if let Some(helper_text) = helper_text {
let limits = limits
.width(width)
.shrink(padding)
.height(helper_text_line_height.to_absolute(helper_text_size.into()));
let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY);
let state = tree.state.downcast_mut::<State>();
let helper_text_paragraph = &mut state.helper_text;
helper_text_paragraph.update(Text {
content: helper_text,
font,
bounds: text_bounds,
size: iced::Pixels(helper_text_size),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
line_height: helper_text_line_height,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
let helper_text_size = helper_text_paragraph.min_bounds();
let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos);
nodes.push(helper_text_node);
};
let mut size = nodes.iter().fold(Size::ZERO, |size, node| {
Size::new(
size.width.max(node.bounds().width),
size.height + node.bounds().height,
)
});
size.height += (nodes.len() - 1) as f32 * f32::from(spacing);
let limits = limits
.width(width)
.height(size.height)
.min_width(size.width);
layout::Node::with_children(limits.resolve(width, size.height, size), nodes)
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_possible_truncation)]
pub fn update<'a, Message: 'static>(
id: Option<Id>,
event: Event,
text_layout: Layout<'_>,
trailing_icon_layout: Option<Layout<'_>>,
cursor: mouse::Cursor,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
value: &mut Value,
size: f32,
font: <crate::Renderer as iced_core::text::Renderer>::Font,
is_secure: bool,
is_editable: bool,
on_input: Option<&dyn Fn(String) -> Message>,
on_paste: Option<&dyn Fn(String) -> Message>,
on_submit: &Option<Message>,
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_id: u128,
line_height: text::LineHeight,
layout: Layout<'_>,
) -> event::Status
where
Message: Clone,
{
let update_cache = |state, value| {
replace_paragraph(state, layout, value, font, iced::Pixels(size), line_height);
};
let mut secured_value = if is_secure {
value.secure()
} else {
value.clone()
};
let unsecured_value = value;
let value = &mut secured_value;
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
};
if click_position.is_some() {
state.is_focused = state.is_focused.or_else(|| {
let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
Some(Focus {
updated_at: now,
now,
})
});
}
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, mouse::Button::Left, state.last_click);
match (
&state.dragging_state,
click.kind(),
state.cursor().state(value),
) {
#[cfg(feature = "wayland")]
(None, click::Kind::Single, cursor::State::Selection { start, end }) => {
if let Some(on_input) = on_input {
let left = start.min(end);
let right = end.max(start);
let (left_position, _left_offset) = measure_cursor_and_scroll_offset(
state.value.raw(),
text_layout.bounds(),
left,
);
let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
state.value.raw(),
text_layout.bounds(),
right,
);
let width = right_position - left_position;
let selection_bounds = Rectangle {
x: text_layout.bounds().x + left_position,
y: text_layout.bounds().y,
width,
height: text_layout.bounds().height,
};
if cursor.is_over(selection_bounds) {
if is_secure {
return event::Status::Ignored;
}
let input_text =
state.selected_text(&value.to_string()).unwrap_or_default();
state.dragging_state = Some(DraggingState::Dnd(
DndAction::empty(),
input_text.clone(),
));
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.delete();
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
if let Some(on_start_dnd) = on_start_dnd_source {
shell.publish(on_start_dnd(state.clone()));
}
let state_clone = state.clone();
iced_core::clipboard::start_dnd(
clipboard,
false,
id.map(|id| iced_core::clipboard::DndSource::Widget(id)),
Some((
Element::from(
TextInput::<'static, ()>::new("", input_text.clone())
.dnd_icon(true),
),
iced_core::widget::tree::State::new(state_clone),
)),
Box::new(TextInputString(input_text)),
DndAction::Move,
);
update_cache(state, &unsecured_value);
} else {
update_cache(state, value);
let position = if target > 0.0 {
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
state.dragging_state = Some(DraggingState::Selection);
}
} else {
let position = if target > 0.0 {
update_cache(state, value);
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
state.dragging_state = Some(DraggingState::Selection);
}
}
(None, click::Kind::Single, _) => {
let position = if target > 0.0 {
update_cache(state, value);
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
state.dragging_state = Some(DraggingState::Selection);
}
(None | Some(DraggingState::Selection), click::Kind::Double, _) => {
update_cache(state, value);
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.dragging_state = Some(DraggingState::Selection);
}
(None | Some(DraggingState::Selection), click::Kind::Triple, _) => {
update_cache(state, value);
state.cursor.select_all(value);
state.dragging_state = Some(DraggingState::Selection);
}
_ => {
state.dragging_state = None;
}
}
if is_editable
&& state.is_read_only
&& matches!(state.dragging_state, None | Some(DraggingState::Selection))
{
state.is_read_only = false;
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();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
state.is_focused = Some(Focus {
updated_at: now,
now,
});
state.move_cursor_to_end();
return event::Status::Captured;
}
}
state.last_click = Some(click);
return event::Status::Captured;
}
let mut is_trailing_clicked = false;
if is_editable {
if let Some(trailing_layout) = trailing_icon_layout {
is_trailing_clicked = cursor.is_over(trailing_layout.bounds());
if is_trailing_clicked && on_toggle_edit.is_some() {
let Some(pos) = cursor.position() else {
return event::Status::Ignored;
};
let click = mouse::Click::new(pos, mouse::Button::Left, 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();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
state.is_focused = Some(Focus {
updated_at: now,
now,
});
state.move_cursor_to_end();
return event::Status::Captured;
}
}
_ => {
state.dragging_state = None;
}
}
}
}
}
if !is_trailing_clicked && click_position.is_none() {
state.is_focused = None;
state.dragging_state = None;
state.is_pasting = None;
state.keyboard_modifiers = keyboard::Modifiers::default();
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
let state = state();
state.dragging_state = None;
}
Event::Mouse(mouse::Event::CursorMoved { position })
| Event::Touch(touch::Event::FingerMoved { position, .. }) => {
let state = state();
if matches!(state.dragging_state, Some(DraggingState::Selection)) {
let target = position.x - text_layout.bounds().x;
update_cache(state, value);
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;
};
if state.is_read_only {
return event::Status::Ignored;
}
let modifiers = state.keyboard_modifiers;
focus.updated_at = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
match key {
keyboard::Key::Named(keyboard::key::Named::Enter) => {
if let Some(on_submit) = on_submit.clone() {
shell.publish(on_submit);
}
}
keyboard::Key::Named(keyboard::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(unsecured_value, &mut state.cursor);
editor.backspace();
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(editor.contents());
shell.publish(message);
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
}
keyboard::Key::Named(keyboard::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(unsecured_value);
state.cursor.select_range(cursor_pos, unsecured_value.len());
} else {
state.cursor.select_right_by_words(unsecured_value);
}
}
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.delete();
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
}
keyboard::Key::Named(keyboard::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(keyboard::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(keyboard::key::Named::Home) => {
if modifiers.shift() {
state.cursor.select_range(state.cursor.start(value), 0);
} else {
state.cursor.move_to(0);
}
}
keyboard::Key::Named(keyboard::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::Character(ref c)
if "c" == c && state.keyboard_modifiers.command() =>
{
if !is_secure {
if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(
iced_core::clipboard::Kind::Primary,
value.select(start, end).to_string(),
);
}
}
}
keyboard::Key::Character(c)
if "x" == c && state.keyboard_modifiers.command() =>
{
if !is_secure {
if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(
iced_core::clipboard::Kind::Primary,
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);
}
}
keyboard::Key::Character(c)
if "v" == c && state.keyboard_modifiers.command() =>
{
let content = if let Some(content) = state.is_pasting.take() {
content
} else {
let content: String = clipboard
.read(iced_core::clipboard::Kind::Primary)
.unwrap_or_default()
.chars()
.filter(|c| !c.is_control())
.collect();
Value::new(&content)
};
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.paste(content.clone());
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = if let Some(paste) = &on_paste {
(paste)(contents)
} else {
(on_input)(contents)
};
shell.publish(message);
state.is_pasting = Some(content);
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
}
keyboard::Key::Character(c)
if "a" == c && state.keyboard_modifiers.command() =>
{
state.cursor.select_all(value);
}
keyboard::Key::Named(keyboard::key::Named::Escape) => {
state.is_focused = None;
state.dragging_state = None;
state.is_pasting = None;
state.keyboard_modifiers = keyboard::Modifiers::default();
}
keyboard::Key::Named(
keyboard::key::Named::Tab
| keyboard::key::Named::ArrowUp
| keyboard::key::Named::ArrowDown,
) => {
return event::Status::Ignored;
}
keyboard::Key::Character(_)
| keyboard::Key::Named(keyboard::key::Named::Space) => {
if state.is_pasting.is_none()
&& !state.keyboard_modifiers.command()
&& !modifiers.control()
{
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.insert(
text.unwrap_or_default().chars().next().unwrap_or_default(),
);
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
focus.updated_at = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
return event::Status::Captured;
}
}
_ => {}
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
let state = state();
if state.is_focused.is_some() {
match key {
keyboard::Key::Character(c) if "v" == c => {
state.is_pasting = None;
}
keyboard::Key::Named(keyboard::key::Named::Tab)
| keyboard::Key::Named(keyboard::key::Named::ArrowUp)
| keyboard::Key::Named(keyboard::key::Named::ArrowDown) => {
return event::Status::Ignored;
}
_ => {}
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = state();
state.keyboard_modifiers = modifiers;
}
Event::Window(window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
focus.now = now;
let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
- (now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS;
shell.request_redraw(window::RedrawRequest::At(
now + Duration::from_millis(u64::try_from(millis_until_redraw).unwrap()),
));
}
}
#[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
let state = state();
if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
state.dragging_state = None;
return event::Status::Captured;
}
}
#[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Offer(
rectangle,
OfferEvent::Enter {
x,
y,
mime_types,
surface,
},
)) if rectangle == Some(dnd_id) => {
let state = state();
let is_clicked = text_layout.bounds().contains(Point {
x: x as f32,
y: y as f32,
});
let mut accepted = false;
for m in &mime_types {
if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
let clone = m.clone();
accepted = true;
}
}
if accepted {
let target = x as f32 - text_layout.bounds().x;
state.dnd_offer =
DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
let position = if target > 0.0 {
update_cache(state, value);
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
return event::Status::Captured;
}
}
#[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y }))
if rectangle == Some(dnd_id) =>
{
let state = state();
let target = x as f32 - text_layout.bounds().x;
let position = if target > 0.0 {
update_cache(state, value);
find_cursor_position(text_layout.bounds(), value, state, target)
} else {
None
};
state.cursor.move_to(position.unwrap_or(0));
return event::Status::Captured;
}
#[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => {
let state = state();
if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES
.iter()
.find(|m| mime_types.contains(&(**m).to_string()))
else {
state.dnd_offer = DndOfferState::None;
return event::Status::Captured;
};
state.dnd_offer = DndOfferState::Dropped;
}
return event::Status::Ignored;
}
#[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Offer(
rectangle,
OfferEvent::Leave | OfferEvent::LeaveDestination,
)) if rectangle == Some(dnd_id) => {
let state = state();
match state.dnd_offer {
DndOfferState::Dropped => {}
_ => {
state.dnd_offer = DndOfferState::None;
}
};
return event::Status::Captured;
}
#[cfg(feature = "wayland")]
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type }))
if rectangle == Some(dnd_id) =>
{
let state = state();
if let DndOfferState::Dropped = state.dnd_offer.clone() {
state.dnd_offer = DndOfferState::None;
if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() {
return event::Status::Captured;
}
let Ok(content) = String::from_utf8(data) else {
return event::Status::Captured;
};
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.paste(Value::new(content.as_str()));
let contents = editor.contents();
let unsecured_value = Value::new(&contents);
if let Some(on_paste) = on_paste.as_ref() {
let message = (on_paste)(contents);
shell.publish(message);
}
let value = if is_secure {
unsecured_value.secure()
} else {
unsecured_value
};
update_cache(state, &value);
return event::Status::Captured;
}
return event::Status::Ignored;
}
_ => {}
}
event::Status::Ignored
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
#[allow(clippy::missing_panics_doc)]
pub fn draw<'a, Message>(
renderer: &mut crate::Renderer,
theme: &crate::Theme,
layout: Layout<'_>,
text_layout: Layout<'_>,
cursor_position: mouse::Cursor,
tree: &Tree,
value: &Value,
placeholder: &str,
size: Option<f32>,
font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
is_disabled: bool,
is_secure: bool,
icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>,
trailing_icon: Option<&Element<'a, Message, crate::Theme, crate::Renderer>>,
style: &<crate::Theme as StyleSheet>::Style,
dnd_icon: bool,
line_height: text::LineHeight,
error: Option<&str>,
label: Option<&str>,
helper_text: Option<&str>,
helper_text_size: f32,
helper_line_height: text::LineHeight,
viewport: &Rectangle,
renderer_style: &renderer::Style,
) {
let children = &tree.children;
let state = tree.state.downcast_ref::<State>();
let secure_value = is_secure.then(|| value.secure());
let value = secure_value.as_ref().unwrap_or(value);
let mut children_layout = layout.children();
let (label_layout, layout, helper_text_layout) = if label.is_some() && helper_text.is_some() {
let label_layout = children_layout.next();
let layout = children_layout.next().unwrap();
let helper_text_layout = children_layout.next();
(label_layout, layout, helper_text_layout)
} else if label.is_some() {
let label_layout = children_layout.next();
let layout = children_layout.next().unwrap();
(label_layout, layout, None)
} else if helper_text.is_some() {
let layout = children_layout.next().unwrap();
let helper_text_layout = children_layout.next();
(None, layout, helper_text_layout)
} else {
let layout = children_layout.next().unwrap();
(None, layout, None)
};
let mut children_layout = layout.children();
let bounds = layout.bounds();
let text_bounds = children_layout.next().unwrap_or(text_layout).bounds();
let is_mouse_over = cursor_position.is_over(bounds);
let appearance = if is_disabled {
theme.disabled(style)
} else if error.is_some() {
theme.error(style)
} else if state.is_focused() {
theme.focused(style)
} else if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
let mut icon_color = appearance.icon_color.unwrap_or(renderer_style.icon_color);
let mut text_color = appearance.text_color.unwrap_or(renderer_style.text_color);
if is_disabled {
let background = theme.current_container().component.base.into();
icon_color = icon_color.blend_alpha(background, 0.5);
text_color = text_color.blend_alpha(background, 0.5);
}
if let Some(border_offset) = appearance.border_offset {
let offset_bounds = Rectangle {
x: bounds.x - border_offset,
y: bounds.y - border_offset,
width: border_offset.mul_add(2.0, bounds.width),
height: border_offset.mul_add(2.0, bounds.height),
};
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
radius: appearance.border_radius,
width: appearance.border_width,
..Default::default()
},
shadow: Shadow {
offset: Vector::new(0.0, 1.0),
color: Color::TRANSPARENT,
blur_radius: 0.0,
},
},
appearance.background,
);
renderer.fill_quad(
renderer::Quad {
bounds: offset_bounds,
border: Border {
width: appearance.border_width,
color: appearance.border_color,
radius: appearance.border_radius,
},
shadow: Shadow {
offset: Vector::new(0.0, 1.0),
color: Color::TRANSPARENT,
blur_radius: 0.0,
},
},
Background::Color(Color::TRANSPARENT),
);
} else {
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
width: appearance.border_width,
color: appearance.border_color,
radius: appearance.border_radius,
},
shadow: Shadow {
offset: Vector::new(0.0, 1.0),
color: Color::TRANSPARENT,
blur_radius: 0.0,
},
},
appearance.background,
);
}
if let (Some(label_layout), Some(label)) = (label_layout, label) {
renderer.fill_text(
Text {
content: label.to_string(),
size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)),
font: font.unwrap_or_else(|| renderer.default_font()),
bounds: label_layout.bounds().size(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
line_height,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
},
label_layout.bounds().position(),
appearance.label_color,
*viewport,
);
}
let mut child_index = 0;
let leading_icon_tree = children.get(child_index);
let has_start_icon = icon.is_some();
if let (Some(icon), Some(tree)) = (icon, leading_icon_tree) {
let mut children = text_layout.children();
let _ = children.next().unwrap();
let icon_layout = children.next().unwrap();
icon.as_widget().draw(
tree,
renderer,
theme,
&renderer::Style {
icon_color,
text_color,
scale_factor: renderer_style.scale_factor,
},
icon_layout,
cursor_position,
viewport,
);
child_index += 1;
}
let text = value.to_string();
let font = font.unwrap_or_else(|| renderer.default_font());
let size = size.unwrap_or_else(|| renderer.default_size().0);
let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into();
#[cfg(feature = "wayland")]
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
#[cfg(not(feature = "wayland"))]
let handling_dnd_offer = false;
let (cursor, offset) = if let Some(focus) = &state.is_focused.or_else(|| {
handling_dnd_offer.then(|| Focus {
updated_at: Instant::now(),
now: Instant::now(),
})
}) {
match state.cursor.state(value) {
cursor::State::Index(position) => {
let (text_value_width, offset) =
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position);
let is_cursor_visible = handling_dnd_offer
|| ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0;
if is_cursor_visible {
if dnd_icon {
(None, 0.0)
} else {
(
Some((
renderer::Quad {
bounds: Rectangle {
x: text_bounds.x + text_value_width - offset,
y: text_bounds.y,
width: 1.0,
height: text_bounds.height,
},
border: Border {
width: 0.0,
color: Color::TRANSPARENT,
radius: radius_0,
},
shadow: Shadow {
offset: Vector::ZERO,
color: Color::TRANSPARENT,
blur_radius: 0.0,
},
},
text_color,
)),
offset,
)
}
} else {
(None, offset)
}
}
cursor::State::Selection { start, end } => {
let left = start.min(end);
let right = end.max(start);
let value_paragraph = &state.value;
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right);
let width = right_position - left_position;
if dnd_icon {
(None, 0.0)
} else {
(
Some((
renderer::Quad {
bounds: Rectangle {
x: text_bounds.x + left_position,
y: text_bounds.y,
width,
height: text_bounds.height,
},
border: Border {
width: 0.0,
color: Color::TRANSPARENT,
radius: radius_0,
},
shadow: Shadow {
offset: Vector::ZERO,
color: Color::TRANSPARENT,
blur_radius: 0.0,
},
},
appearance.selected_fill,
)),
if end == right {
right_offset
} else {
left_offset
},
)
}
}
}
} else {
(None, 0.0)
};
let text_width = state.value.min_width();
let render = |renderer: &mut crate::Renderer| {
if let Some((cursor, color)) = cursor {
renderer.fill_quad(cursor, color);
} else {
renderer.with_translation(Vector::ZERO, |_| {});
}
let bounds = Rectangle {
x: text_bounds.x - offset,
y: text_bounds.center_y(),
width: f32::INFINITY,
..text_bounds
};
let color = if text.is_empty() {
appearance.placeholder_color
} else {
text_color
};
renderer.fill_text(
Text {
content: if text.is_empty() {
placeholder.to_string()
} else {
text.clone()
},
font,
bounds: bounds.size(),
size: iced::Pixels(size),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
},
bounds.position(),
color,
*viewport,
);
};
renderer.with_layer(text_bounds, render);
let trailing_icon_tree = children.get(child_index);
if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) {
let mut children = text_layout.children();
let mut icon_layout = children.next().unwrap();
if has_start_icon {
icon_layout = children.next().unwrap();
}
icon_layout = children.next().unwrap();
icon.as_widget().draw(
tree,
renderer,
theme,
&renderer::Style {
icon_color,
text_color,
scale_factor: renderer_style.scale_factor,
},
icon_layout,
cursor_position,
viewport,
);
}
if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) {
renderer.fill_text(
Text {
content: helper_text.to_string(), size: iced::Pixels(helper_text_size),
font,
bounds: helper_text_layout.bounds().size(),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
line_height: helper_line_height,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
},
helper_text_layout.bounds().position(),
text_color,
*viewport,
);
}
}
#[must_use]
pub fn mouse_interaction(
layout: Layout<'_>,
cursor_position: mouse::Cursor,
is_disabled: bool,
) -> mouse::Interaction {
if cursor_position.is_over(layout.bounds()) {
if is_disabled {
mouse::Interaction::NotAllowed
} else {
mouse::Interaction::Text
}
} else {
mouse::Interaction::default()
}
}
#[derive(Debug, Clone)]
pub struct TextInputString(pub String);
#[cfg(feature = "wayland")]
impl AsMimeTypes for TextInputString {
fn available(&self) -> Cow<'static, [String]> {
Cow::Owned(
SUPPORTED_TEXT_MIME_TYPES
.iter()
.cloned()
.map(String::from)
.collect::<Vec<_>>(),
)
}
fn as_bytes(&self, mime_type: &str) -> Option<Cow<'static, [u8]>> {
if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) {
Some(Cow::Owned(self.0.clone().as_bytes().to_vec()))
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DraggingState {
Selection,
#[cfg(feature = "wayland")]
Dnd(DndAction, String),
}
#[cfg(feature = "wayland")]
#[derive(Debug, Default, Clone)]
pub(crate) enum DndOfferState {
#[default]
None,
HandlingOffer(Vec<String>, DndAction),
Dropped,
}
#[derive(Debug, Default, Clone)]
#[cfg(not(feature = "wayland"))]
pub(crate) struct DndOfferState;
#[derive(Debug, Default, Clone)]
#[must_use]
pub struct State {
pub value: crate::Plain,
pub placeholder: crate::Plain,
pub label: crate::Plain,
pub helper_text: crate::Plain,
pub dirty: bool,
pub is_secure: bool,
pub is_read_only: bool,
select_on_focus: bool,
is_focused: Option<Focus>,
dragging_state: Option<DraggingState>,
dnd_offer: DndOfferState,
is_pasting: Option<Value>,
last_click: Option<mouse::Click>,
cursor: Cursor,
keyboard_modifiers: keyboard::Modifiers,
}
#[derive(Debug, Clone, Copy)]
struct Focus {
updated_at: Instant,
now: Instant,
}
impl State {
pub fn new(
is_secure: bool,
is_read_only: bool,
always_active: bool,
select_on_focus: bool,
) -> Self {
Self {
is_secure,
is_read_only,
is_focused: always_active.then(|| {
let now = Instant::now();
Focus {
updated_at: now,
now,
}
}),
select_on_focus,
..Self::default()
}
}
#[must_use]
pub fn selected_text(&self, text: &str) -> Option<String> {
let value = Value::new(text);
match self.cursor.state(&value) {
cursor::State::Index(_) => None,
cursor::State::Selection { start, end } => {
let left = start.min(end);
let right = end.max(start);
Some(text[left..right].to_string())
}
}
}
#[cfg(feature = "wayland")]
#[must_use]
pub fn dragged_text(&self) -> Option<String> {
match self.dragging_state.as_ref() {
Some(DraggingState::Dnd(_, text)) => Some(text.clone()),
_ => None,
}
}
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
Self {
is_secure,
value: crate::Plain::default(),
placeholder: crate::Plain::default(),
label: crate::Plain::default(),
helper_text: crate::Plain::default(),
is_read_only,
is_focused: None,
select_on_focus: false,
dragging_state: None,
dnd_offer: DndOfferState::default(),
is_pasting: None,
last_click: None,
cursor: Cursor::default(),
keyboard_modifiers: keyboard::Modifiers::default(),
dirty: false,
}
}
#[must_use]
pub fn is_focused(&self) -> bool {
self.is_focused.is_some()
}
#[must_use]
pub fn cursor(&self) -> Cursor {
self.cursor
}
pub fn focus(&mut self) {
let now = Instant::now();
LAST_FOCUS_UPDATE.with(|x| x.set(now));
self.is_read_only = false;
self.is_focused = Some(Focus {
updated_at: now,
now,
});
if self.select_on_focus {
self.select_all()
} else {
self.move_cursor_to_end();
}
}
pub fn unfocus(&mut self) {
self.is_focused = None;
}
pub fn move_cursor_to_front(&mut self) {
self.cursor.move_to(0);
}
pub fn move_cursor_to_end(&mut self) {
self.cursor.move_to(usize::MAX);
}
pub fn move_cursor_to(&mut self, position: usize) {
self.cursor.move_to(position);
}
pub fn select_all(&mut self) {
self.cursor.select_range(0, usize::MAX);
}
}
impl operation::Focusable for State {
fn is_focused(&self) -> bool {
Self::is_focused(self)
}
fn focus(&mut self) {
Self::focus(self);
}
fn unfocus(&mut self) {
Self::unfocus(self);
}
}
impl operation::TextInput for State {
fn move_cursor_to_front(&mut self) {
Self::move_cursor_to_front(self);
}
fn move_cursor_to_end(&mut self) {
Self::move_cursor_to_end(self);
}
fn move_cursor_to(&mut self, position: usize) {
Self::move_cursor_to(self, position);
}
fn select_all(&mut self) {
Self::select_all(self);
}
}
fn measure_cursor_and_scroll_offset(
paragraph: &impl text::Paragraph,
text_bounds: Rectangle,
cursor_index: usize,
) -> (f32, f32) {
let grapheme_position = paragraph
.grapheme_position(0, cursor_index)
.unwrap_or(Point::ORIGIN);
let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
(grapheme_position.x, offset)
}
fn find_cursor_position(
text_bounds: Rectangle,
value: &Value,
state: &State,
x: f32,
) -> Option<usize> {
let offset = offset(text_bounds, value, state);
let value = value.to_string();
let char_offset = state
.value
.raw()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
Some(
unicode_segmentation::UnicodeSegmentation::graphemes(
&value[..char_offset.min(value.len())],
true,
)
.count(),
)
}
fn replace_paragraph(
state: &mut State,
layout: Layout<'_>,
value: &Value,
font: <crate::Renderer as iced_core::text::Renderer>::Font,
text_size: Pixels,
line_height: text::LineHeight,
) {
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
state.value = crate::Plain::new(Text {
font,
line_height,
content: &value.to_string(),
bounds: Size::new(f32::INFINITY, text_bounds.height),
size: text_size,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::default(),
});
}
const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
mod platform {
use iced_core::keyboard;
pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool {
if cfg!(target_os = "macos") {
modifiers.alt()
} else {
modifiers.control()
}
}
}
fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
if state.is_focused() {
let cursor = state.cursor();
let focus_position = match cursor.state(value) {
cursor::State::Index(i) => i,
cursor::State::Selection { end, .. } => end,
};
let (_, offset) =
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position);
offset
} else {
0.0
}
}