Merge upstream/master (COSMIC 1.6) into main
Aligne le fork sur epoch-1.6.0 (63 commits amont) en preservant les 19 commits yoda. Resolutions notables : - les cfg `all(feature = "wayland", target_os = "linux")` deviennent l alias `wayland_platform` introduit en amont ; il exclut winit comme le faisait le patch yoda45eaaf16, l intention est donc preservee ; - les features iced `cctk` ont ete renommees `wayland` en amont, la feature `wayland` locale est adaptee ; `winit` reste un marqueur vide et `iced/winit` est liste explicitement ; - cctk/ashpd gardent leurs chemins locaux dans la section conditionnelle unix introduite en amont ; - la garde anti-crashc0b653f5(context_menu non assigne) est conservee par-dessus la nouvelle structure amont ; - traductions ca/oc restaurees depuis amont. Le submodule iced suit (merge + correctif du cfg cctk residuel). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
9535ada4d0
60 changed files with 3187 additions and 1528 deletions
|
|
@ -43,6 +43,12 @@ thread_local! {
|
|||
static LAST_FOCUS_UPDATE: LazyCell<Cell<Instant>> = LazyCell::new(|| Cell::new(Instant::now()));
|
||||
}
|
||||
|
||||
/// Notify all text inputs that a different widget has taken focus.
|
||||
/// This causes any focused text input to unfocus on its next layout.
|
||||
pub fn notify_focus_change() {
|
||||
LAST_FOCUS_UPDATE.with(|x| x.set(Instant::now()));
|
||||
}
|
||||
|
||||
/// Creates a new [`TextInput`].
|
||||
///
|
||||
/// [`TextInput`]: widget::TextInput
|
||||
|
|
@ -212,6 +218,7 @@ pub struct TextInput<'a, Message> {
|
|||
/// The text input tracks and manages the input value in its state.
|
||||
manage_value: bool,
|
||||
drag_threshold: f32,
|
||||
window_id: window::Id,
|
||||
}
|
||||
|
||||
impl<'a, Message> TextInput<'a, Message>
|
||||
|
|
@ -262,6 +269,7 @@ where
|
|||
always_active: false,
|
||||
manage_value: false,
|
||||
drag_threshold: 20.0,
|
||||
window_id: crate::widget::text_context_menu::current_window_id(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -525,7 +533,7 @@ where
|
|||
}
|
||||
|
||||
/// Sets the start dnd handler of the [`TextInput`].
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
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
|
||||
|
|
@ -577,6 +585,17 @@ where
|
|||
self.drag_threshold = drag_threshold;
|
||||
self
|
||||
}
|
||||
|
||||
fn uses_popup_context_menu(&self) -> bool {
|
||||
#[cfg(all(wayland_platform, feature = "winit"))]
|
||||
if matches!(
|
||||
crate::app::cosmic::WINDOWING_SYSTEM.get(),
|
||||
Some(crate::app::cosmic::WindowingSystem::Wayland)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, crate::Theme, crate::Renderer> for TextInput<'_, Message>
|
||||
|
|
@ -840,6 +859,24 @@ where
|
|||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
|
||||
if !self.uses_popup_context_menu() {
|
||||
let has_context_menu = tree
|
||||
.state
|
||||
.downcast_ref::<State>()
|
||||
.context_menu_position
|
||||
.is_some();
|
||||
if has_context_menu {
|
||||
let menu_bar_state = tree.state.downcast_ref::<State>().menu_bar_state.clone();
|
||||
return crate::widget::text_context_menu::context_menu_overlay(
|
||||
self,
|
||||
tree,
|
||||
self.on_input.as_deref(),
|
||||
translation,
|
||||
menu_bar_state,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut layout_ = Vec::with_capacity(2);
|
||||
if self.leading_icon.is_some() {
|
||||
let mut children = self.text_layout(layout).children();
|
||||
|
|
@ -854,7 +891,7 @@ where
|
|||
}
|
||||
layout_.push(children.next().unwrap());
|
||||
};
|
||||
let children = self
|
||||
let children: Vec<overlay::Element<'_, Message, crate::Theme, crate::Renderer>> = self
|
||||
.leading_icon
|
||||
.iter_mut()
|
||||
.chain(self.trailing_icon.iter_mut())
|
||||
|
|
@ -865,7 +902,7 @@ where
|
|||
.as_widget_mut()
|
||||
.overlay(state, layout, renderer, viewport, translation)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
(!children.is_empty()).then(|| Group::with_children(children).overlay())
|
||||
}
|
||||
|
|
@ -881,6 +918,16 @@ where
|
|||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
#[cfg(all(wayland_platform, feature = "winit"))]
|
||||
if self.uses_popup_context_menu() {
|
||||
let menu_bar_state = tree.state.downcast_ref::<State>().menu_bar_state.clone();
|
||||
crate::widget::text_context_menu::dismiss_popup_on_event(
|
||||
&menu_bar_state,
|
||||
event,
|
||||
self.window_id,
|
||||
);
|
||||
}
|
||||
|
||||
let text_layout = self.text_layout(layout);
|
||||
let mut trailing_icon_layout = None;
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
|
|
@ -931,6 +978,23 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// Unfocus on any click outside widget bounds.
|
||||
if matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::ButtonPressed(_))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. })
|
||||
) && cursor_position.position_over(layout.bounds()).is_none()
|
||||
{
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
state.is_focused = None;
|
||||
state.context_menu_position = None;
|
||||
state.dragging_state = None;
|
||||
if let Some(on_unfocus) = self.on_unfocus.as_ref() {
|
||||
shell.publish(on_unfocus.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
if let Some(on_unfocus) = self.on_unfocus.as_ref()
|
||||
|
|
@ -971,6 +1035,73 @@ where
|
|||
self.always_active,
|
||||
);
|
||||
|
||||
// On Wayland: if right-click just set context_menu_position, create a popup instead.
|
||||
#[cfg(all(wayland_platform, feature = "winit"))]
|
||||
if matches!(
|
||||
crate::app::cosmic::WINDOWING_SYSTEM.get(),
|
||||
Some(crate::app::cosmic::WindowingSystem::Wayland)
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
if state.context_menu_position.is_some() {
|
||||
let selected_text = state
|
||||
.cursor()
|
||||
.selection(&state.tracked_value)
|
||||
.map(|(start, end)| state.tracked_value.select(start, end).to_string());
|
||||
let has_selection = selected_text.is_some();
|
||||
let click_position = state.context_menu_position.unwrap();
|
||||
let menu_bar_state = state.menu_bar_state.clone();
|
||||
let pending_action = state.pending_action.clone();
|
||||
|
||||
crate::widget::text_context_menu::create_text_context_popup(
|
||||
click_position,
|
||||
selected_text,
|
||||
true,
|
||||
has_selection,
|
||||
&menu_bar_state,
|
||||
&pending_action,
|
||||
renderer,
|
||||
viewport,
|
||||
cursor_position,
|
||||
self.window_id,
|
||||
);
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
state.context_menu_position = None;
|
||||
}
|
||||
|
||||
// Process deferred actions from the popup.
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let pending_action = state.pending_action.clone();
|
||||
if let Some(action) =
|
||||
crate::widget::text_context_menu::take_pending_action(&pending_action)
|
||||
{
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
match action {
|
||||
crate::widget::text_context_menu::TextCtxAction::Copy => {}
|
||||
crate::widget::text_context_menu::TextCtxAction::Cut => {
|
||||
let contents = state.delete_selection();
|
||||
if let Some(on_input) = self.on_input.as_deref() {
|
||||
shell.publish((on_input)(contents));
|
||||
}
|
||||
}
|
||||
crate::widget::text_context_menu::TextCtxAction::Paste => {
|
||||
let content: String = clipboard
|
||||
.read(iced_core::clipboard::Kind::Standard)
|
||||
.unwrap_or_default();
|
||||
let filtered: String =
|
||||
content.chars().filter(|c| !c.is_control()).collect();
|
||||
let contents = state.paste_text(&filtered);
|
||||
if let Some(on_input) = self.on_input.as_deref() {
|
||||
shell.publish((on_input)(contents));
|
||||
}
|
||||
}
|
||||
crate::widget::text_context_menu::TextCtxAction::SelectAll => {
|
||||
state.select_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
let value = if self.is_secure {
|
||||
self.value.secure()
|
||||
|
|
@ -1455,11 +1586,44 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
#[cold]
|
||||
fn cold() {}
|
||||
|
||||
let state = state();
|
||||
|
||||
// Any click outside clears focus and selection.
|
||||
if matches!(
|
||||
event,
|
||||
Event::Mouse(mouse::Event::ButtonPressed(_))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. })
|
||||
) && cursor.position_over(layout.bounds()).is_none()
|
||||
{
|
||||
state.is_focused = None;
|
||||
state.context_menu_position = None;
|
||||
state.dragging_state = None;
|
||||
if let Some(on_unfocus) = on_unfocus {
|
||||
shell.publish(on_unfocus.clone());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => {
|
||||
if let Some(pos) = cursor.position_over(layout.bounds()) {
|
||||
if !state.is_focused() {
|
||||
state.focus();
|
||||
}
|
||||
state.context_menu_position = Some(pos);
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
cold();
|
||||
let state = state();
|
||||
|
||||
if state.context_menu_position.take().is_some() {
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
|
||||
let click_position = if on_input.is_some() || manage_value {
|
||||
cursor.position_over(layout.bounds())
|
||||
|
|
@ -1547,7 +1711,7 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
click.kind(),
|
||||
state.cursor().state(value),
|
||||
) {
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
(None, click::Kind::Single, cursor::State::Selection { start, end }) => {
|
||||
let left = start.min(end);
|
||||
let right = end.max(start);
|
||||
|
|
@ -1673,6 +1837,7 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
|
||||
state.last_click = Some(click);
|
||||
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
} else {
|
||||
state.unfocus();
|
||||
|
|
@ -1685,8 +1850,7 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
|
||||
cold();
|
||||
let state = state();
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) {
|
||||
// clear selection and place cursor at click position
|
||||
update_cache(state, value);
|
||||
|
|
@ -1712,8 +1876,6 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
}
|
||||
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 = {
|
||||
let text_bounds = text_layout.bounds();
|
||||
|
|
@ -1737,10 +1899,11 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
.cursor
|
||||
.select_range(state.cursor.start(value), position);
|
||||
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state {
|
||||
let distance = ((position.x - start_position.x).powi(2)
|
||||
+ (position.y - start_position.y).powi(2))
|
||||
|
|
@ -1800,7 +1963,6 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
modifiers,
|
||||
..
|
||||
}) => {
|
||||
let state = state();
|
||||
state.keyboard_modifiers = *modifiers;
|
||||
|
||||
if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) {
|
||||
|
|
@ -2088,17 +2250,19 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
_ => {}
|
||||
}
|
||||
|
||||
shell.request_redraw();
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
|
||||
let state = state();
|
||||
|
||||
if state.is_focused() {
|
||||
match key {
|
||||
keyboard::Key::Character(c) if "v" == c => {
|
||||
state.is_pasting = None;
|
||||
}
|
||||
keyboard::Key::Named(keyboard::key::Named::Insert) => {
|
||||
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) => {
|
||||
|
|
@ -2111,70 +2275,64 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
let state = state();
|
||||
|
||||
state.keyboard_modifiers = *modifiers;
|
||||
}
|
||||
Event::InputMethod(event) => {
|
||||
let state = state();
|
||||
|
||||
match event {
|
||||
input_method::Event::Opened | input_method::Event::Closed => {
|
||||
state.preedit = matches!(event, input_method::Event::Opened)
|
||||
.then(input_method::Preedit::new);
|
||||
shell.capture_event();
|
||||
}
|
||||
input_method::Event::Preedit(content, selection) => {
|
||||
if state.is_focused() {
|
||||
state.preedit = Some(input_method::Preedit {
|
||||
content: content.to_owned(),
|
||||
selection: selection.clone(),
|
||||
text_size: Some(size.into()),
|
||||
});
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
input_method::Event::Commit(text) => {
|
||||
let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else {
|
||||
return;
|
||||
};
|
||||
let Some(on_input) = on_input else {
|
||||
return;
|
||||
};
|
||||
if state.is_read_only {
|
||||
return;
|
||||
}
|
||||
|
||||
focus.updated_at = Instant::now();
|
||||
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
|
||||
|
||||
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
|
||||
editor.paste(Value::new(text));
|
||||
|
||||
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 = None;
|
||||
let value = if is_secure {
|
||||
unsecured_value.secure()
|
||||
} else {
|
||||
unsecured_value
|
||||
};
|
||||
|
||||
update_cache(state, &value);
|
||||
Event::InputMethod(event) => match event {
|
||||
input_method::Event::Opened | input_method::Event::Closed => {
|
||||
state.preedit =
|
||||
matches!(event, input_method::Event::Opened).then(input_method::Preedit::new);
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
input_method::Event::Preedit(content, selection) => {
|
||||
if state.is_focused() {
|
||||
state.preedit = Some(input_method::Preedit {
|
||||
content: content.to_owned(),
|
||||
selection: selection.clone(),
|
||||
text_size: Some(size.into()),
|
||||
});
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Window(window::Event::RedrawRequested(now)) => {
|
||||
let state = state();
|
||||
input_method::Event::Commit(text) => {
|
||||
let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else {
|
||||
return;
|
||||
};
|
||||
let Some(on_input) = on_input else {
|
||||
return;
|
||||
};
|
||||
if state.is_read_only {
|
||||
return;
|
||||
}
|
||||
|
||||
focus.updated_at = Instant::now();
|
||||
LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at));
|
||||
|
||||
let mut editor = Editor::new(unsecured_value, &mut state.cursor);
|
||||
editor.paste(Value::new(&text));
|
||||
|
||||
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 = None;
|
||||
let value = if is_secure {
|
||||
unsecured_value.secure()
|
||||
} else {
|
||||
unsecured_value
|
||||
};
|
||||
|
||||
update_cache(state, &value);
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
},
|
||||
Event::Window(window::Event::RedrawRequested(now)) => {
|
||||
if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) {
|
||||
focus.now = *now;
|
||||
|
||||
|
|
@ -2190,17 +2348,16 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => {
|
||||
cold();
|
||||
let state = state();
|
||||
if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) {
|
||||
// TODO: restore value in text input
|
||||
state.dragging_state = None;
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Offer(
|
||||
rectangle,
|
||||
OfferEvent::Enter {
|
||||
|
|
@ -2211,8 +2368,7 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
},
|
||||
)) if *rectangle == Some(dnd_id) => {
|
||||
cold();
|
||||
let state = state();
|
||||
let _is_clicked = text_layout.bounds().contains(Point {
|
||||
let is_clicked = text_layout.bounds().contains(Point {
|
||||
x: *x as f32,
|
||||
y: *y as f32,
|
||||
});
|
||||
|
|
@ -2249,12 +2405,10 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y: _ }))
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y }))
|
||||
if *rectangle == Some(dnd_id) =>
|
||||
{
|
||||
let state = state();
|
||||
|
||||
let target = {
|
||||
let text_bounds = text_layout.bounds();
|
||||
|
||||
|
|
@ -2276,10 +2430,9 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
state.cursor.move_to(position);
|
||||
shell.capture_event();
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => {
|
||||
cold();
|
||||
let state = state();
|
||||
if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() {
|
||||
let Some(_mime_type) = SUPPORTED_TEXT_MIME_TYPES
|
||||
.iter()
|
||||
|
|
@ -2292,15 +2445,14 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
state.dnd_offer = DndOfferState::Dropped;
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Offer(
|
||||
_rectangle,
|
||||
OfferEvent::Leave | OfferEvent::LeaveDestination,
|
||||
)) => {
|
||||
cold();
|
||||
let state = state();
|
||||
// ASHLEY TODO we should be able to reset but for now we don't if we are handling a
|
||||
// drop
|
||||
match state.dnd_offer {
|
||||
|
|
@ -2311,12 +2463,11 @@ pub fn update<'a, Message: Clone + 'static>(
|
|||
};
|
||||
shell.capture_event();
|
||||
}
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type }))
|
||||
if *rectangle == Some(dnd_id) =>
|
||||
{
|
||||
cold();
|
||||
let state = state();
|
||||
if matches!(&state.dnd_offer, DndOfferState::Dropped) {
|
||||
state.dnd_offer = DndOfferState::None;
|
||||
if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() {
|
||||
|
|
@ -2592,9 +2743,9 @@ pub fn draw<'a, Message>(
|
|||
let actual_width = text_width.max(text_bounds.width);
|
||||
|
||||
let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into();
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
|
||||
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
|
||||
#[cfg(not(wayland_platform))]
|
||||
let handling_dnd_offer = false;
|
||||
let (cursors, offset, _is_selecting) = if let Some(focus) =
|
||||
state.is_focused.filter(|f| f.focused).or_else(|| {
|
||||
|
|
@ -2844,7 +2995,7 @@ pub fn mouse_interaction(
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct TextInputString(pub String);
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
impl AsMimeTypes for TextInputString {
|
||||
fn available(&self) -> Cow<'static, [String]> {
|
||||
Cow::Owned(
|
||||
|
|
@ -2868,13 +3019,13 @@ impl AsMimeTypes for TextInputString {
|
|||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum DraggingState {
|
||||
Selection,
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
PrepareDnd(Point),
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
Dnd(DndAction, String),
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) enum DndOfferState {
|
||||
#[default]
|
||||
|
|
@ -2883,11 +3034,11 @@ pub(crate) enum DndOfferState {
|
|||
Dropped,
|
||||
}
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg(not(all(feature = "wayland", target_os = "linux")))]
|
||||
#[cfg(not(wayland_platform))]
|
||||
pub(crate) struct DndOfferState;
|
||||
|
||||
/// The state of a [`TextInput`].
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Default, Clone)]
|
||||
#[must_use]
|
||||
pub struct State {
|
||||
pub tracked_value: Value,
|
||||
|
|
@ -2910,6 +3061,19 @@ pub struct State {
|
|||
preedit: Option<Preedit>,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
scroll_offset: f32,
|
||||
context_menu_position: Option<iced_core::Point>,
|
||||
pub(crate) menu_bar_state: crate::widget::menu::MenuBarState,
|
||||
pub(crate) pending_action: crate::widget::text_context_menu::PendingAction,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for State {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("State")
|
||||
.field("is_secure", &self.is_secure)
|
||||
.field("is_read_only", &self.is_read_only)
|
||||
.field("dirty", &self.dirty)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
|
|
@ -2954,12 +3118,12 @@ impl State {
|
|||
cursor::State::Selection { start, end } => {
|
||||
let left = start.min(end);
|
||||
let right = end.max(start);
|
||||
Some(text[left..right].to_string())
|
||||
Some(value.select(left, right).to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "wayland", target_os = "linux"))]
|
||||
#[cfg(wayland_platform)]
|
||||
/// Returns the current value of the dragged text in the [`TextInput`].
|
||||
#[must_use]
|
||||
pub fn dragged_text(&self) -> Option<String> {
|
||||
|
|
@ -2992,6 +3156,9 @@ impl State {
|
|||
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||
scroll_offset: 0.0,
|
||||
dirty: false,
|
||||
context_menu_position: None,
|
||||
menu_bar_state: crate::widget::menu::MenuBarState::default(),
|
||||
pending_action: crate::widget::text_context_menu::pending_action(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3036,7 +3203,7 @@ impl State {
|
|||
/// Unfocuses the [`TextInput`].
|
||||
#[cold]
|
||||
pub(super) fn unfocus(&mut self) {
|
||||
self.move_cursor_to_front();
|
||||
self.cursor.clear_selection();
|
||||
self.last_click = None;
|
||||
self.is_focused = self.is_focused.map(|mut f| {
|
||||
f.focused = false;
|
||||
|
|
@ -3078,6 +3245,33 @@ impl State {
|
|||
self.cursor.select_range(start, end);
|
||||
}
|
||||
|
||||
/// Returns the context menu position, if a context menu should be shown.
|
||||
pub fn context_menu_position(&self) -> Option<iced_core::Point> {
|
||||
self.context_menu_position
|
||||
}
|
||||
|
||||
/// Sets or clears the context menu position.
|
||||
pub fn set_context_menu_position(&mut self, pos: Option<iced_core::Point>) {
|
||||
self.context_menu_position = pos;
|
||||
}
|
||||
|
||||
/// Deletes the current selection and returns the new text content.
|
||||
pub fn delete_selection(&mut self) -> String {
|
||||
let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor);
|
||||
editor.delete();
|
||||
editor.contents()
|
||||
}
|
||||
|
||||
/// Pastes text at the current cursor position and returns the new text content.
|
||||
pub fn paste_text(&mut self, text: &str) -> String {
|
||||
let paste_value = super::value::Value::new(text);
|
||||
let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor);
|
||||
editor.paste(paste_value);
|
||||
let contents = editor.contents();
|
||||
self.tracked_value = super::value::Value::new(&contents);
|
||||
contents
|
||||
}
|
||||
|
||||
pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle<f32>, target: f32) {
|
||||
let (position, affinity) = find_cursor_position(bounds, value, self, target)
|
||||
.unwrap_or((0, text::Affinity::Before));
|
||||
|
|
@ -3305,3 +3499,47 @@ fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizonta
|
|||
alignment::Horizontal::Left
|
||||
}
|
||||
}
|
||||
|
||||
use iced_core::widget::tree::Tree as WidgetTree;
|
||||
|
||||
impl<Message: Clone + 'static> iced_core::widget::text::HasSelectableText
|
||||
for TextInput<'_, Message>
|
||||
{
|
||||
fn selected_text(&self, tree: &WidgetTree) -> Option<String> {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
let (start, end) = state.cursor().selection(&state.tracked_value)?;
|
||||
Some(state.tracked_value.select(start, end).to_string())
|
||||
}
|
||||
|
||||
fn select_all(&self, tree: &mut WidgetTree) {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
state.select_all();
|
||||
}
|
||||
|
||||
fn is_editable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_focused(&self, tree: &WidgetTree) -> bool {
|
||||
tree.state.downcast_ref::<State>().is_focused()
|
||||
}
|
||||
|
||||
fn context_menu_position(&self, tree: &WidgetTree) -> Option<iced_core::Point> {
|
||||
tree.state.downcast_ref::<State>().context_menu_position
|
||||
}
|
||||
|
||||
fn set_context_menu_position(&self, tree: &mut WidgetTree, pos: Option<iced_core::Point>) {
|
||||
tree.state.downcast_mut::<State>().context_menu_position = pos;
|
||||
}
|
||||
|
||||
fn delete_selection(&self, tree: &mut WidgetTree) -> Option<String> {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
Some(state.delete_selection())
|
||||
}
|
||||
|
||||
fn paste_text(&self, tree: &mut WidgetTree, text: &str) -> Option<String> {
|
||||
let filtered: String = text.chars().filter(|c| !c.is_control()).collect();
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
Some(state.paste_text(&filtered))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue