libcosmic updates

This commit is contained in:
Ashley Wulber 2024-10-16 20:36:46 -04:00 committed by Ashley Wulber
parent 9c62f19e4b
commit 0491c4baaa
91 changed files with 3550 additions and 2300 deletions

View file

@ -18,6 +18,9 @@ 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};
@ -39,15 +42,9 @@ use iced_core::{
};
#[cfg(feature = "wayland")]
use iced_renderer::core::event::{wayland, PlatformSpecific};
use iced_renderer::core::widget::OperationOutputWrapper;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific;
use iced_runtime::Command;
#[cfg(feature = "wayland")]
use cctk::sctk::reexports::client::protocol::wl_data_device_manager::DndAction;
#[cfg(feature = "wayland")]
use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon};
use iced_runtime::platform_specific;
use iced_runtime::{task, Action, Task};
thread_local! {
// Prevents two inputs from being focused at the same time.
@ -146,7 +143,7 @@ where
})
.size(16)
.apply(crate::widget::button::custom)
.style(crate::theme::Button::Icon)
.class(crate::theme::Button::Icon)
.on_press(msg)
.padding(8)
.into(),
@ -173,7 +170,6 @@ where
.padding(spacing)
}
#[cfg(feature = "wayland")]
pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[
"text/plain;charset=utf-8",
"text/plain;charset=UTF-8",
@ -182,17 +178,12 @@ pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[
"text/plain",
"TEXT",
];
#[cfg(feature = "wayland")]
pub type DnDCommand =
Box<dyn Send + Sync + Fn() -> platform_specific::wayland::data_device::ActionInner>;
#[cfg(not(feature = "wayland"))]
pub type DnDCommand = ();
/// A field that can be filled with text.
#[allow(missing_debug_implementations)]
#[must_use]
pub struct TextInput<'a, Message> {
id: Option<Id>,
id: Id,
placeholder: Cow<'a, str>,
value: Value,
is_secure: bool,
@ -215,7 +206,6 @@ pub struct TextInput<'a, Message> {
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>>,
on_dnd_command_produced: Option<Box<dyn Fn(DnDCommand) -> Message + 'a>>,
surface_ids: Option<(window::Id, window::Id)>,
dnd_icon: bool,
line_height: text::LineHeight,
@ -237,7 +227,7 @@ where
let v: Cow<'a, str> = value.into();
TextInput {
id: None,
id: Id::unique(),
placeholder: placeholder.into(),
value: Value::new(v.as_ref()),
is_secure: false,
@ -258,7 +248,6 @@ where
trailing_icon: None,
error: None,
style: crate::theme::TextInput::default(),
on_dnd_command_produced: None,
on_create_dnd_source: None,
surface_ids: None,
dnd_icon: false,
@ -269,6 +258,15 @@ where
}
}
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!(),
}
}
/// Sets the input to be always active.
/// This makes it behave as if it was always focused.
pub fn always_active(mut self) -> Self {
@ -290,7 +288,7 @@ where
/// Sets the [`Id`] of the [`TextInput`].
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self.id = id;
self
}
@ -433,10 +431,12 @@ where
value: Option<&Value>,
style: &renderer::Style,
) {
let text_layout = self.text_layout(layout.clone());
draw(
renderer,
theme,
layout,
text_layout,
cursor_position,
tree,
value.unwrap_or(&self.value),
@ -467,20 +467,6 @@ where
self
}
/// Sets the dnd command produced handler of the [`TextInput`].
/// Commands should be returned in the update function of the application.
#[cfg(feature = "wayland")]
pub fn on_dnd_command_produced(
mut self,
on_dnd_command_produced: impl Fn(
Box<dyn Send + Sync + Fn() -> platform_specific::wayland::data_device::ActionInner>,
) -> Message
+ 'a,
) -> Self {
self.on_dnd_command_produced = Some(Box::new(on_dnd_command_produced));
self
}
/// Sets the window id of the [`TextInput`] and the window id of the drag icon.
/// Both ids are required to be unique.
/// This is required for the dnd to work.
@ -500,7 +486,7 @@ where
crate::widget::icon::from_name("edit-clear-symbolic")
.size(16)
.apply(crate::widget::button::custom)
.style(crate::theme::Button::Icon)
.class(crate::theme::Button::Icon)
.on_press(on_clear)
.padding(8)
.into(),
@ -550,6 +536,7 @@ where
}
let old_value = state
.value
.raw()
.buffer()
.lines
.iter()
@ -559,6 +546,7 @@ where
|| old_value != self.value.to_string()
|| state
.label
.raw()
.buffer()
.lines
.iter()
@ -567,6 +555,7 @@ where
!= self.label.unwrap_or_default()
|| state
.helper_text
.raw()
.buffer()
.lines
.iter()
@ -651,7 +640,7 @@ where
let v = self.value.to_string();
value_paragraph.update(Text {
content: if self.value.is_empty() {
&self.placeholder
self.placeholder.as_ref()
} else {
&v
},
@ -662,7 +651,7 @@ where
vertical_alignment: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
let Size { width, height } =
@ -717,12 +706,13 @@ where
tree: &mut Tree,
_layout: Layout<'_>,
_renderer: &crate::Renderer,
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
operation: &mut dyn Operation<()>,
) {
let state = tree.state.downcast_mut::<State>();
operation.focusable(state, self.id.as_ref());
operation.text_input(state, self.id.as_ref());
operation.custom(state, Some(&self.id));
operation.focusable(state, Some(&self.id));
operation.text_input(state, Some(&self.id));
}
fn overlay<'b>(
@ -730,6 +720,7 @@ where
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() {
@ -752,7 +743,9 @@ where
.zip(&mut tree.children)
.zip(layout_)
.filter_map(|((child, state), layout)| {
child.as_widget_mut().overlay(state, layout, renderer)
child
.as_widget_mut()
.overlay(state, layout, renderer, translation)
})
.collect::<Vec<_>>();
@ -814,8 +807,10 @@ where
}
}
}
let dnd_id = self.dnd_id();
let id = Widget::id(self);
update(
id,
event,
text_layout.children().next().unwrap(),
trailing_icon_layout,
@ -833,9 +828,7 @@ where
self.on_toggle_edit.as_deref(),
|| tree.state.downcast_mut::<State>(),
self.on_create_dnd_source.as_deref(),
self.dnd_icon,
self.on_dnd_command_produced.as_deref(),
self.surface_ids,
dnd_id,
line_height,
layout,
)
@ -851,10 +844,12 @@ where
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let text_layout = self.text_layout(layout.clone());
draw(
renderer,
theme,
layout,
text_layout,
cursor_position,
tree,
&self.value,
@ -934,11 +929,43 @@ where
}
fn id(&self) -> Option<Id> {
self.id.clone()
Some(self.id.clone())
}
fn set_id(&mut self, id: Id) {
self.id = Some(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,
});
}
}
}
@ -954,32 +981,38 @@ where
}
}
/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::focusable::focus(id))
/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`].
pub fn focus<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::focusable::focus(id)))
}
/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// end.
pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::text_input::move_cursor_to_end(id))
pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to_end(
id,
)))
}
/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// front.
pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::text_input::move_cursor_to_front(id))
pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::move_cursor_to_front(
id,
)))
}
/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the
/// provided position.
pub fn move_cursor_to<Message: 'static>(id: Id, position: usize) -> Command<Message> {
Command::widget(operation::text_input::move_cursor_to(id, position))
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,
)))
}
/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`].
pub fn select_all<Message: 'static>(id: Id) -> Command<Message> {
Command::widget(operation::text_input::select_all(id))
/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`].
pub fn select_all<Message: 'static>(id: Id) -> Task<Message> {
task::effect(Action::widget(operation::text_input::select_all(id)))
}
/// Computes the layout of a [`TextInput`].
@ -1019,7 +1052,7 @@ pub fn layout<Message>(
vertical_alignment: alignment::Vertical::Center,
line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
let label_size = label_paragraph.min_bounds();
@ -1158,7 +1191,7 @@ pub fn layout<Message>(
vertical_alignment: alignment::Vertical::Center,
line_height: helper_text_line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
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);
@ -1188,7 +1221,8 @@ pub fn layout<Message>(
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_possible_truncation)]
pub fn update<'a, Message>(
pub fn update<'a, Message: 'static>(
id: Option<Id>,
event: Event,
text_layout: Layout<'_>,
trailing_icon_layout: Option<Layout<'_>>,
@ -1206,9 +1240,7 @@ pub fn update<'a, 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_icon: bool,
#[allow(unused_variables)] on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>,
#[allow(unused_variables)] surface_ids: Option<(window::Id, window::Id)>,
#[allow(unused_variables)] dnd_id: u128,
line_height: text::LineHeight,
layout: Layout<'_>,
) -> event::Status
@ -1253,7 +1285,8 @@ where
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);
let click =
mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click);
match (
&state.dragging_state,
@ -1266,28 +1299,18 @@ where
// single click that is on top of the selected text
// is the click on selected text?
if let (
Some(on_start_dnd),
Some(on_dnd_command_produced),
Some((window_id, icon_id)),
Some(on_input),
) = (
on_start_dnd_source,
on_dnd_command_produced,
surface_ids,
on_input,
) {
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,
state.value.raw(),
text_layout.bounds(),
left,
);
let (right_position, _right_offset) = measure_cursor_and_scroll_offset(
&state.value,
state.value.raw(),
text_layout.bounds(),
right,
);
@ -1305,10 +1328,12 @@ where
if is_secure {
return event::Status::Ignored;
}
let text =
let input_text =
state.selected_text(&value.to_string()).unwrap_or_default();
state.dragging_state =
Some(DraggingState::Dnd(DndAction::empty(), text.clone()));
state.dragging_state = Some(DraggingState::Dnd(
DndAction::empty(),
input_text.clone(),
));
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
editor.delete();
@ -1316,23 +1341,25 @@ where
let unsecured_value = Value::new(&contents);
let message = (on_input)(contents);
shell.publish(message);
shell.publish(on_start_dnd(state.clone()));
if let Some(on_start_dnd) = on_start_dnd_source {
shell.publish(on_start_dnd(state.clone()));
}
let state_clone = state.clone();
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::StartDnd {
mime_types: SUPPORTED_TEXT_MIME_TYPES
.iter()
.map(std::string::ToString::to_string)
.collect(),
actions: DndAction::Move,
origin_id: window_id,
icon_id: Some((
DndIcon::Widget(icon_id, Box::new(state_clone.clone())),
iced::Vector::ZERO,
)),
data: Box::new(TextInputString(text.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 {
@ -1435,7 +1462,7 @@ where
return event::Status::Ignored;
};
let click = mouse::Click::new(pos, state.last_click);
let click = mouse::Click::new(pos, mouse::Button::Left, state.last_click);
match (
&state.dragging_state,
@ -1621,7 +1648,10 @@ where
{
if !is_secure {
if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(value.select(start, end).to_string());
clipboard.write(
iced_core::clipboard::Kind::Primary,
value.select(start, end).to_string(),
);
}
}
}
@ -1632,7 +1662,10 @@ where
{
if !is_secure {
if let Some((start, end)) = state.cursor.selection(value) {
clipboard.write(value.select(start, end).to_string());
clipboard.write(
iced_core::clipboard::Kind::Primary,
value.select(start, end).to_string(),
);
}
let mut editor = Editor::new(value, &mut state.cursor);
@ -1650,7 +1683,7 @@ where
content
} else {
let content: String = clipboard
.read()
.read(iced_core::clipboard::Kind::Primary)
.unwrap_or_default()
.chars()
.filter(|c| !c.is_control())
@ -1760,7 +1793,7 @@ where
state.keyboard_modifiers = modifiers;
}
Event::Window(_, window::Event::RedrawRequested(now)) => {
Event::Window(window::Event::RedrawRequested(now)) => {
let state = state();
if let Some(focus) = &mut state.is_focused {
@ -1775,9 +1808,7 @@ where
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource(
wayland::DataSourceEvent::DndFinished | wayland::DataSourceEvent::Cancelled,
))) => {
Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
let state = state();
if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
state.dragging_state = None;
@ -1785,54 +1816,32 @@ where
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource(
wayland::DataSourceEvent::DndActionAccepted(action),
))) => {
let state = state();
if let Some(DraggingState::Dnd(_, text)) = state.dragging_state.as_ref() {
state.dragging_state = Some(DraggingState::Dnd(action, text.clone()));
return event::Status::Captured;
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::Enter { x, y, mime_types },
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
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,
});
if !is_clicked {
state.dnd_offer = DndOfferState::OutsideWidget(mime_types, DndAction::None);
return event::Status::Captured;
}
let mut accepted = false;
for m in &mime_types {
if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
let clone = m.clone();
accepted = true;
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::Accept(Some(
clone.clone(),
))
})));
}
}
if accepted {
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: DndAction::Move,
accepted: DndAction::Move.union(DndAction::Copy),
}
})));
let target = x as f32 - text_layout.bounds().x;
state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None);
state.dnd_offer =
DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty());
// existing logic for setting the selection
let position = if target > 0.0 {
update_cache(state, value);
@ -1846,57 +1855,11 @@ where
}
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::Motion { x, y },
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y }))
if rectangle == Some(dnd_id) =>
{
let state = state();
let is_clicked = text_layout.bounds().contains(Point {
x: x as f32,
y: y as f32,
});
if !is_clicked {
if let DndOfferState::HandlingOffer(mime_types, action) = state.dnd_offer.clone() {
state.dnd_offer = DndOfferState::OutsideWidget(mime_types, action);
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: DndAction::None,
accepted: DndAction::None,
}
})));
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::Accept(None)
})));
}
return event::Status::Captured;
} else if let DndOfferState::OutsideWidget(mime_types, action) = state.dnd_offer.clone()
{
let mut accepted = false;
for m in &mime_types {
if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) {
accepted = true;
let clone = m.clone();
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::Accept(Some(
clone.clone(),
))
})));
}
}
if accepted {
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: DndAction::Move,
accepted: DndAction::Move.union(DndAction::Copy),
}
})));
state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), action);
}
};
let target = x as f32 - text_layout.bounds().x;
// existing logic for setting the selection
let position = if target > 0.0 {
@ -1910,13 +1873,7 @@ where
return event::Status::Captured;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::DropPerformed,
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
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
@ -1927,21 +1884,15 @@ where
return event::Status::Captured;
};
state.dnd_offer = DndOfferState::Dropped;
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::RequestDndData(
(*mime_type).to_string(),
)
})));
} else if let DndOfferState::OutsideWidget(..) = &state.dnd_offer {
state.dnd_offer = DndOfferState::None;
return event::Status::Captured;
}
return event::Status::Ignored;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::Leave,
))) => {
Event::Dnd(DndEvent::Offer(
rectangle,
OfferEvent::Leave | OfferEvent::LeaveDestination,
)) if rectangle == Some(dnd_id) => {
let state = state();
// ASHLEY TODO we should be able to reset but for now we don't if we are handling a
// drop
@ -1954,13 +1905,9 @@ where
return event::Status::Captured;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::DndData { mime_type, data },
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
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;
@ -1982,9 +1929,6 @@ where
shell.publish(message);
}
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::DndFinished
})));
let value = if is_secure {
unsecured_value.secure()
} else {
@ -1995,26 +1939,6 @@ where
}
return event::Status::Ignored;
}
#[cfg(feature = "wayland")]
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer(
wayland::DndOfferEvent::SourceActions(actions),
))) => {
let Some(on_dnd_command_produced) = on_dnd_command_produced else {
return event::Status::Ignored;
};
let state = state();
if let DndOfferState::HandlingOffer(..) = state.dnd_offer.clone() {
shell.publish(on_dnd_command_produced(Box::new(move || {
platform_specific::wayland::data_device::ActionInner::SetActions {
preferred: actions.intersection(DndAction::Move),
accepted: actions,
}
})));
return event::Status::Captured;
}
return event::Status::Ignored;
}
_ => {}
}
@ -2032,6 +1956,7 @@ 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,
@ -2083,7 +2008,7 @@ pub fn draw<'a, Message>(
let mut children_layout = layout.children();
let bounds = layout.bounds();
let text_bounds = children_layout.next().unwrap().bounds();
let text_bounds = text_layout.bounds();
let is_mouse_over = cursor_position.is_over(bounds);
@ -2172,7 +2097,7 @@ pub fn draw<'a, Message>(
if let (Some(label_layout), Some(label)) = (label_layout, label) {
renderer.fill_text(
Text {
content: label,
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(),
@ -2180,7 +2105,7 @@ pub fn draw<'a, Message>(
vertical_alignment: alignment::Vertical::Top,
line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
label_layout.bounds().position(),
appearance.label_color,
@ -2214,14 +2139,24 @@ pub fn draw<'a, Message>(
let size = size.unwrap_or_else(|| renderer.default_size().0);
let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into();
let (cursor, offset) = if let Some(focus) = &state.is_focused {
#[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, text_bounds, position);
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position);
let is_cursor_visible =
((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) % 2
let is_cursor_visible = handling_dnd_offer
|| ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS)
% 2
== 0;
if is_cursor_visible {
@ -2263,10 +2198,10 @@ pub fn draw<'a, Message>(
let value_paragraph = &state.value;
let (left_position, left_offset) =
measure_cursor_and_scroll_offset(value_paragraph, text_bounds, left);
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left);
let (right_position, right_offset) =
measure_cursor_and_scroll_offset(value_paragraph, text_bounds, right);
measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right);
let width = right_position - left_position;
@ -2331,7 +2266,11 @@ pub fn draw<'a, Message>(
renderer.fill_text(
Text {
content: if text.is_empty() { placeholder } else { &text },
content: if text.is_empty() {
placeholder.to_string()
} else {
text.clone()
},
font,
bounds: bounds.size(),
size: iced::Pixels(size),
@ -2339,7 +2278,7 @@ pub fn draw<'a, Message>(
vertical_alignment: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
bounds.position(),
color,
@ -2374,7 +2313,7 @@ pub fn draw<'a, Message>(
if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) {
renderer.fill_text(
Text {
content: helper_text,
content: helper_text.to_string(), // TODO remove to_string?
size: iced::Pixels(helper_text_size),
font,
bounds: helper_text_layout.bounds().size(),
@ -2382,7 +2321,7 @@ pub fn draw<'a, Message>(
vertical_alignment: alignment::Vertical::Top,
line_height: helper_line_height,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
},
helper_text_layout.bounds().position(),
text_color,
@ -2414,11 +2353,23 @@ pub fn mouse_interaction(
pub struct TextInputString(pub String);
#[cfg(feature = "wayland")]
impl DataFromMimeType for TextInputString {
fn from_mime_type(&self, mime_type: &str) -> Option<Vec<u8>> {
SUPPORTED_TEXT_MIME_TYPES
.contains(&mime_type)
.then(|| self.0.as_bytes().to_vec())
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
}
}
}
@ -2434,7 +2385,6 @@ pub(crate) enum DraggingState {
pub(crate) enum DndOfferState {
#[default]
None,
OutsideWidget(Vec<String>, DndAction),
HandlingOffer(Vec<String>, DndAction),
Dropped,
}
@ -2446,17 +2396,16 @@ pub(crate) struct DndOfferState;
#[derive(Debug, Default, Clone)]
#[must_use]
pub struct State {
pub value: crate::Paragraph,
pub placeholder: crate::Paragraph,
pub label: crate::Paragraph,
pub helper_text: crate::Paragraph,
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>,
#[cfg(feature = "wayland")]
dnd_offer: DndOfferState,
is_pasting: Option<Value>,
last_click: Option<mouse::Click>,
@ -2522,15 +2471,14 @@ impl State {
pub fn focused(is_secure: bool, is_read_only: bool) -> Self {
Self {
is_secure,
value: crate::Paragraph::new(),
placeholder: crate::Paragraph::new(),
label: crate::Paragraph::new(),
helper_text: crate::Paragraph::new(),
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,
#[cfg(feature = "wayland")]
dnd_offer: DndOfferState::default(),
is_pasting: None,
last_click: None,
@ -2654,6 +2602,7 @@ fn find_cursor_position(
let char_offset = state
.value
.raw()
.hit_test(Point::new(x + offset, text_bounds.height / 2.0))
.map(text::Hit::cursor)?;
@ -2677,7 +2626,7 @@ fn replace_paragraph(
let mut children_layout = layout.children();
let text_bounds = children_layout.next().unwrap().bounds();
state.value = crate::Paragraph::with_text(Text {
state.value = crate::Plain::new(Text {
font,
line_height,
content: &value.to_string(),
@ -2686,7 +2635,7 @@ fn replace_paragraph(
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top,
shaping: text::Shaping::Advanced,
wrap: text::Wrap::default(),
wrapping: text::Wrapping::default(),
});
}
@ -2714,7 +2663,7 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 {
};
let (_, offset) =
measure_cursor_and_scroll_offset(&state.value, text_bounds, focus_position);
measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position);
offset
} else {