From 0e797b244043ee86610113d547950204258dea83 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 11 Sep 2025 01:46:03 -0400 Subject: [PATCH 1/7] improv(input): better initial handling of focus state --- src/widget/text_input/input.rs | 53 ++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index b8c035d4..3a2af337 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -591,7 +591,10 @@ where // Unfocus text input if it becomes disabled if self.on_input.is_none() && !self.manage_value { state.last_click = None; - state.is_focused = None; + state.is_focused = state.is_focused.map(|mut f| { + f.focused = false; + f + }); state.is_pasting = None; state.dragging_state = None; } @@ -628,18 +631,19 @@ where state.dirty = true; } - if self.always_active && state.is_focused.is_none() { + if self.always_active && !state.is_focused() { let now = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(now)); state.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); } // if the previous state was at the end of the text, keep it there let old_value = Value::new(&old_value); - if state.is_focused.is_some() { + if state.is_focused() { if let cursor::State::Index(index) = state.cursor.state(&old_value) { if index == old_value.len() { state.cursor.move_to(self.value.len()); @@ -647,7 +651,7 @@ where }; } - if let Some(f) = state.is_focused.as_ref() { + if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) { if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { state.unfocus(); state.emit_unfocus = true; @@ -838,9 +842,12 @@ where if self.is_editable_variant { if let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); - if !state.is_read_only && state.is_focused.is_none() { + if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; shell.publish((on_edit)(false)); + } else if state.is_focused() && state.is_read_only { + state.is_read_only = false; + shell.publish((on_edit)(true)); } } } @@ -1392,6 +1399,7 @@ pub fn update<'a, Message: Clone + 'static>( state.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); } @@ -1520,7 +1528,7 @@ pub fn update<'a, Message: Clone + 'static>( } // Focus on click of the text input, and ensure that the input is writable. - if state.is_focused.is_none() + if !state.is_focused() && matches!(state.dragging_state, None | Some(DraggingState::Selection)) { if let Some(on_focus) = on_focus { @@ -1541,6 +1549,7 @@ pub fn update<'a, Message: Clone + 'static>( state.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); } @@ -1592,8 +1601,7 @@ pub fn update<'a, Message: Clone + 'static>( .. }) => { let state = state(); - - if let Some(focus) = &mut state.is_focused { + if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { if state.is_read_only || (!manage_value && on_input.is_none()) { return event::Status::Ignored; }; @@ -1873,7 +1881,7 @@ pub fn update<'a, Message: Clone + 'static>( Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state(); - if state.is_focused.is_some() { + if state.is_focused() { match key { keyboard::Key::Character(c) if "v" == c => { state.is_pasting = None; @@ -1897,7 +1905,7 @@ pub fn update<'a, Message: Clone + 'static>( Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); - if let Some(focus) = &mut state.is_focused { + if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { focus.now = now; let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS @@ -2258,12 +2266,15 @@ pub fn draw<'a, Message>( 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(), - }) - }) { + let (cursor, offset) = if let Some(focus) = + state.is_focused.filter(|f| f.focused).or_else(|| { + let now = Instant::now(); + handling_dnd_offer.then(|| Focus { + updated_at: now, + now, + focused: true, + }) + }) { match state.cursor.state(value) { cursor::State::Index(position) => { let (text_value_width, offset) = @@ -2547,6 +2558,7 @@ pub struct State { struct Focus { updated_at: Instant, now: Instant, + focused: bool, } impl State { @@ -2565,6 +2577,7 @@ impl State { Focus { updated_at: now, now, + focused: true, } }), select_on_focus, @@ -2623,7 +2636,7 @@ impl State { #[inline] #[must_use] pub fn is_focused(&self) -> bool { - self.is_focused.is_some() + self.is_focused.is_some_and(|f| f.focused) } /// Returns the [`Cursor`] of the [`TextInput`]. @@ -2642,6 +2655,7 @@ impl State { self.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); if self.select_on_focus { @@ -2656,7 +2670,10 @@ impl State { pub(super) fn unfocus(&mut self) { self.move_cursor_to_front(); self.last_click = None; - self.is_focused = None; + self.is_focused = self.is_focused.map(|mut f| { + f.focused = false; + f + }); self.dragging_state = None; self.is_pasting = None; self.keyboard_modifiers = keyboard::Modifiers::default(); From c01254dd18c95f05742690f04964feef7d931192 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 16 Sep 2025 22:54:27 -0400 Subject: [PATCH 2/7] fix(menu): overlays should be used when multi-window is not active --- src/widget/menu/menu_bar.rs | 41 ++++++++++++++++++++++++++++------- src/widget/menu/menu_inner.rs | 24 ++++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 707aebdc..30c802c1 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -9,7 +9,12 @@ use super::{ }, menu_tree::MenuTree, }; -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::{ Renderer, @@ -190,7 +195,7 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -225,7 +230,7 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -319,7 +324,7 @@ where self } - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -351,7 +356,12 @@ where self } - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -630,7 +640,12 @@ where if !create_popup { return event::Status::Ignored; } - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -638,7 +653,12 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -715,7 +735,12 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.on_surface_action.is_some() && self.window_id != window::Id::NONE diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 18b4433f..6c694de7 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -4,7 +4,12 @@ use std::{borrow::Cow, sync::Arc}; use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; @@ -663,6 +668,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if needs_reset { #[cfg(all( + feature = "multi-window", feature = "wayland", feature = "winit", feature = "surface-message" @@ -932,7 +938,12 @@ impl Widget event::Status { let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { if let Some((new_root, new_ms)) = new_root { use iced_runtime::platform_specific::wayland::popup::{ @@ -1177,7 +1188,12 @@ pub(crate) fn init_root_menu( }); } -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" +))] pub(super) fn init_root_popup_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, @@ -1474,7 +1490,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { if let Some(id) = state.popup_id.remove(&menu.window_id) { state.active_root.truncate(menu.depth + 1); From 9ff208e9d7b538bc780bd2c4b13d342337780469 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 17 Sep 2025 14:31:41 -0400 Subject: [PATCH 3/7] fix: if editable input is focused by operation, emit a message --- src/widget/text_input/input.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 3a2af337..12e8e7ce 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -638,6 +638,7 @@ where updated_at: now, now, focused: true, + needs_update: false, }); } @@ -838,7 +839,7 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); let line_height = self.line_height; - // Disables editing of the editable variant when clicking outside of it. + // Disables editing of the editable variant when clicking outside of, or for tab focus changes. if self.is_editable_variant { if let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); @@ -848,6 +849,11 @@ where } else if state.is_focused() && state.is_read_only { state.is_read_only = false; shell.publish((on_edit)(true)); + } else if let Some(f) = state.is_focused.as_mut().filter(|f| f.needs_update) { + // TODO do we want to just move this to on_focus or on_unfocus for all inputs? + f.needs_update = false; + state.is_read_only = true; + shell.publish((on_edit)(f.focused)); } } } @@ -1400,6 +1406,7 @@ pub fn update<'a, Message: Clone + 'static>( updated_at: now, now, focused: true, + needs_update: false, }); } @@ -1550,6 +1557,7 @@ pub fn update<'a, Message: Clone + 'static>( updated_at: now, now, focused: true, + needs_update: false, }); } @@ -2270,6 +2278,7 @@ pub fn draw<'a, Message>( state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then(|| Focus { + needs_update: false, updated_at: now, now, focused: true, @@ -2559,6 +2568,7 @@ struct Focus { updated_at: Instant, now: Instant, focused: bool, + needs_update: bool, } impl State { @@ -2578,6 +2588,7 @@ impl State { updated_at: now, now, focused: true, + needs_update: false, } }), select_on_focus, @@ -2656,6 +2667,7 @@ impl State { updated_at: now, now, focused: true, + needs_update: false, }); if self.select_on_focus { @@ -2672,6 +2684,7 @@ impl State { self.last_click = None; self.is_focused = self.is_focused.map(|mut f| { f.focused = false; + f.needs_update = false; f }); self.dragging_state = None; @@ -2724,11 +2737,17 @@ impl operation::Focusable for State { #[inline] fn focus(&mut self) { Self::focus(self); + if let Some(focus) = self.is_focused.as_mut() { + focus.needs_update = true; + } } #[inline] fn unfocus(&mut self) { Self::unfocus(self); + if let Some(focus) = self.is_focused.as_mut() { + focus.needs_update = true; + } } } From 19d273ed2e2058e52a947387c948a8bce7bb39a0 Mon Sep 17 00:00:00 2001 From: jermanuts <109705802+jermanuts@users.noreply.github.com> Date: Sat, 6 Sep 2025 05:54:13 +0200 Subject: [PATCH 4/7] i18n(ar): add Arabic translation --- i18n/ar/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/ar/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl new file mode 100644 index 00000000..4fc8582b --- /dev/null +++ b/i18n/ar/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = أغلق + +# About +license = الترخيص +links = الروابط +developers = المطوّرون +designers = المصمّمون +artists = الفنانون +translators = المترجمون +documenters = الموثّقون From 66df10ad89a3a37183c124bfbef254af108af5ac Mon Sep 17 00:00:00 2001 From: FurkanAdmin <47474630+FurkanAdmin@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:20:39 +0300 Subject: [PATCH 5/7] i18n(tr): add translation --- i18n/tr/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/tr/libcosmic.ftl diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic.ftl new file mode 100644 index 00000000..fd0f5475 --- /dev/null +++ b/i18n/tr/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Kapat + +# About +license = Lisans +links = Bağlantılar +developers = Geliştiriciler +designers = Tasarımcılar +artists = Sanatçılar +translators = Çevirmenler +documenters = Belgelendiriciler From 17fa2cd29a69eb2098eb4f3bb912631ee6cf1df1 Mon Sep 17 00:00:00 2001 From: David Carvalho Date: Sun, 7 Sep 2025 22:47:59 -0300 Subject: [PATCH 6/7] i18n(pt-BR): add translations --- i18n/pt-BR/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/pt-BR/libcosmic.ftl diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl new file mode 100644 index 00000000..febf5b2e --- /dev/null +++ b/i18n/pt-BR/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Fechar + +# About +license = Licença +links = Links +developers = Desenvolvedores +designers = Designers +artists = Artistas +translators = Tradutores +documenters = Documentadores From 31fa09a92a98ccbe47c81c83b41cd20a91107aae Mon Sep 17 00:00:00 2001 From: therealmate <61843503+therealmate@users.noreply.github.com> Date: Fri, 19 Sep 2025 00:21:35 +0200 Subject: [PATCH 7/7] i18n(hu): add translation --- i18n/hu/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/hu/libcosmic.ftl diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl new file mode 100644 index 00000000..ddc43e6c --- /dev/null +++ b/i18n/hu/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Bezárás + +# About +license = Licenc +links = Linkek +developers = Fejlesztők +designers = Tervezők +artists = Művészek +translators = Fordítók +documenters = Dokumentálók