From 4bb0d69ce19800ceac3ff1cec1a92e187d2bc9e4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Dec 2025 13:29:22 -0500 Subject: [PATCH 001/168] Revert "tests: fix env guard and pipe read for tab dnd" This reverts commit ce0868582b3cc09d52de917fef91f50d439fb2a9. --- src/desktop.rs | 11 ++++------- src/process.rs | 30 ++++++++++-------------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 01698af5..82242460 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -881,9 +881,7 @@ mod tests { impl EnvVarGuard { fn set(key: &'static str, value: &Path) -> Self { let original = env::var(key).ok(); - // std::env::{set_var, remove_var} are unsafe on newer toolchains; - // we limit scope here to the test helper that toggles a single key. - unsafe { std::env::set_var(key, value) }; + std::env::set_var(key, value); Self { key, original } } } @@ -891,9 +889,9 @@ mod tests { impl Drop for EnvVarGuard { fn drop(&mut self) { if let Some(ref original) = self.original { - unsafe { std::env::set_var(self.key, original) }; + std::env::set_var(self.key, original); } else { - unsafe { std::env::remove_var(self.key) }; + std::env::remove_var(self.key); } } } @@ -1110,8 +1108,7 @@ Icon=vmware-workstation\n\ let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); assert!(resolved.icon().is_some()); assert!(resolved.exec().is_some()); - let expected = format!("crx_{}", id); - assert_eq!(resolved.startup_wm_class(), Some(expected.as_str())); + assert_eq!(resolved.startup_wm_class(), Some(&format!("crx_{}", id))); } #[test] diff --git a/src/process.rs b/src/process.rs index 2b6c4e0e..1ad048dc 100644 --- a/src/process.rs +++ b/src/process.rs @@ -9,28 +9,18 @@ use std::process::{Command, Stdio, exit}; #[cfg(feature = "tokio")] use tokio::io::AsyncReadExt; +#[cfg(feature = "tokio")] async fn read_from_pipe(read: OwnedFd) -> Option { - #[cfg(feature = "tokio")] - { - let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap(); - return read.read_u32().await.ok(); - } + let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap(); + read.read_u32().await.ok() +} - #[cfg(all(feature = "smol", not(feature = "tokio")))] - { - let mut read = smol::Async::new(std::fs::File::from(read)).unwrap(); - let mut bytes = [0; 4]; - read.read_exact(&mut bytes).await.ok()?; - return Some(u32::from_be_bytes(bytes)); - } - - #[cfg(not(any(feature = "tokio", feature = "smol")))] - { - use rustix::fd::AsFd; - let mut bytes = [0u8; 4]; - rustix::io::read(&read, &mut bytes).ok()?; - return Some(u32::from_be_bytes(bytes)); - } +#[cfg(all(feature = "smol", not(feature = "tokio")))] +async fn read_from_pipe(read: OwnedFd) -> Option { + let mut read = smol::Async::new(std::fs::File::from(read)).unwrap(); + let mut bytes = [0; 4]; + read.read_exact(&mut bytes).await.ok()?; + Some(u32::from_be_bytes(bytes)) } /// Performs a double fork with setsid to spawn and detach a command. From 17a2f6243785816fb532aaea3432b89a9fd89432 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Dec 2025 13:29:54 -0500 Subject: [PATCH 002/168] Revert "segmented button: support tab drag + drop" This reverts commit 7f321cb0a3b5ec53f6a6d33320e4f5d0f737959c. --- Cargo.toml | 1 - src/widget/dnd_destination.rs | 144 +---- src/widget/segmented_button/mod.rs | 13 - src/widget/segmented_button/model/mod.rs | 71 --- src/widget/segmented_button/widget.rs | 753 +---------------------- 5 files changed, 32 insertions(+), 950 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 927444e8..b6a01e32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,6 @@ image = { version = "0.25.8", default-features = false, features = [ "png", ] } libc = { version = "0.2.175", optional = true } -log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" raw-window-handle = "0.6" diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index c943d2c7..ccc0fb18 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -39,7 +39,6 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( } static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); -const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DragId(pub u128); @@ -76,12 +75,6 @@ pub struct DndDestination<'a, Message> { } impl<'a, Message: 'static> DndDestination<'a, Message> { - fn mime_matches(&self, offered: &[String]) -> bool { - self.mime_types.is_empty() - || offered - .iter() - .any(|mime| self.mime_types.iter().any(|allowed| allowed == mime)) - } pub fn new(child: impl Into>, mimes: Vec>) -> Self { Self { id: Id::unique(), @@ -331,12 +324,6 @@ impl Widget let my_id = self.get_drag_id(); - log::trace!( - target: DND_DEST_LOG_TARGET, - "dnd_destination id={:?}: event {:?}", - self.drag_id.unwrap_or_default(), - event - ); match event { Event::Dnd(DndEvent::Offer( id, @@ -344,18 +331,6 @@ impl Widget x, y, mime_types, .. }, )) if id == Some(my_id) => { - if !self.mime_matches(&mime_types) { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", - self.mime_types - ); - return event::Status::Ignored; - } - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}" - ); if let Some(msg) = state.on_enter( x, y, @@ -385,11 +360,6 @@ impl Widget return event::Status::Captured; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer leave id={:?}", - my_id - ); if let Some(msg) = state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) { @@ -413,10 +383,6 @@ impl Widget return event::Status::Ignored; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer motion id={my_id:?} coords=({x},{y})" - ); if let Some(msg) = state.on_motion( x, y, @@ -447,11 +413,6 @@ impl Widget return event::Status::Captured; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer leave-destination id={:?}", - my_id - ); if let Some(msg) = state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) { @@ -460,10 +421,6 @@ impl Widget return event::Status::Ignored; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer drop id={my_id:?}" - ); if let Some(msg) = state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref)) { @@ -474,10 +431,6 @@ impl Widget Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) if id == Some(my_id) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer selected-action id={my_id:?} action={action:?}" - ); if let Some(msg) = state.on_action_selected( action, self.on_action_selected @@ -491,11 +444,6 @@ impl Widget Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) if id == Some(my_id) => { - log::trace!( - target: DND_DEST_LOG_TARGET, - "offer data id={my_id:?} mime={mime_type:?} bytes={}", - data.len() - ); if let (Some(msg), ret) = state.on_data_received( mime_type, data, @@ -573,16 +521,6 @@ impl Widget ) { let bounds = layout.bounds(); let my_id = self.get_drag_id(); - log::trace!( - target: DND_DEST_LOG_TARGET, - "register destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - self.mime_types - ); let my_dest = DndDestinationRectangle { id: my_id, rectangle: dnd::Rectangle { @@ -597,14 +535,12 @@ impl Widget }; dnd_rectangles.push(my_dest); - if let Some(child_layout) = layout.children().next() { - self.container.as_widget().drag_destinations( - &state.children[0], - child_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } + self.container.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); } fn id(&self) -> Option { @@ -760,71 +696,3 @@ impl<'a, Message: 'static> From> for Element<'a, Mes Element::new(wrapper) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Clone, Copy, Debug, PartialEq)] - enum TestMsg { - Data, - Finished, - } - - #[test] - fn data_before_drop_invokes_data_handler_only() { - let mut state: State<()> = State::new(); - assert!(state.drag_offer.is_none()); - state.on_enter::( - 4.0, - 2.0, - vec!["text/plain".into()], - Option:: TestMsg>::None, - (), - ); - let (message, status) = state.on_data_received( - "text/plain".into(), - vec![1], - Some(|mime, data| { - assert_eq!(mime, "text/plain"); - assert_eq!(data, vec![1]); - TestMsg::Data - }), - Option:: TestMsg>::None, - ); - assert!(matches!(message, Some(TestMsg::Data))); - assert_eq!(status, event::Status::Captured); - assert!(state.drag_offer.is_some()); - } - - #[test] - fn finish_only_emits_after_drop() { - let mut state: State<()> = State::new(); - state.on_enter::( - 5.0, - -1.0, - vec![], - Option:: TestMsg>::None, - (), - ); - state.on_action_selected::(DndAction::Move, Option:: TestMsg>::None); - state.on_drop::(Option:: TestMsg>::None); - - let (message, status) = state.on_data_received( - "application/x-test".into(), - vec![7], - Option:: TestMsg>::None, - Some(|mime, data, action, x, y| { - assert_eq!(mime, "application/x-test"); - assert_eq!(data, vec![7]); - assert_eq!(action, DndAction::Move); - assert_eq!(x, 5.0); - assert_eq!(y, -1.0); - TestMsg::Finished - }), - ); - assert!(matches!(message, Some(TestMsg::Finished))); - assert_eq!(status, event::Status::Captured); - assert!(state.drag_offer.is_none()); - } -} diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index 81c71be8..e609d70b 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -88,19 +88,6 @@ pub use self::style::{Appearance, ItemAppearance, ItemStatusAppearance, StyleShe pub use self::vertical::{VerticalSegmentedButton, vertical}; pub use self::widget::{Id, SegmentedButton, SegmentedVariant, focus}; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum InsertPosition { - Before, - After, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ReorderEvent { - pub dragged: Entity, - pub target: Entity, - pub position: InsertPosition, -} - /// Associates extra data with an external secondary map. /// /// The secondary map internally uses a `Vec`, so should only be used for data that diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index e0dd8c54..6b5a8a64 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -11,7 +11,6 @@ mod selection; pub use self::selection::{MultiSelect, Selectable, SingleSelect}; use crate::widget::Icon; -use crate::widget::segmented_button::InsertPosition; use slotmap::{SecondaryMap, SlotMap}; use std::any::{Any, TypeId}; use std::borrow::Cow; @@ -411,36 +410,6 @@ where true } - /// Reorder `dragged` relative to `target` based on the provided position. - /// - /// Returns `true` if the model changed, or `false` if the move was invalid. - pub fn reorder(&mut self, dragged: Entity, target: Entity, position: InsertPosition) -> bool { - if !self.contains_item(dragged) || !self.contains_item(target) || dragged == target { - return false; - } - - let len = self.iter().count(); - let target_pos = self.position(target).map(|pos| pos as usize).unwrap_or(len); - let from_pos = self - .position(dragged) - .map(|pos| pos as usize) - .unwrap_or(target_pos); - let mut insert_pos = match position { - InsertPosition::Before => target_pos, - InsertPosition::After => target_pos.saturating_add(1), - }; - if from_pos < insert_pos { - insert_pos = insert_pos.saturating_sub(1); - } - if len > 0 { - insert_pos = insert_pos.min(len.saturating_sub(1)); - } - - self.position_set(dragged, insert_pos as u16); - self.activate(dragged); - true - } - /// Removes an item from the model. /// /// The generation of the slot for the ID will be incremented, so this ID will no @@ -500,43 +469,3 @@ where self.text.remove(id) } } - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_model() -> (Model, Vec) { - let mut ids = Vec::new(); - let model = Model::builder() - .insert(|b| b.text("Tab1").with_id(|id| ids.push(id))) - .insert(|b| b.text("Tab2").with_id(|id| ids.push(id))) - .insert(|b| b.text("Tab3").with_id(|id| ids.push(id))) - .insert(|b| b.text("Tab4").with_id(|id| ids.push(id))) - .build(); - (model, ids) - } - - fn order_of(model: &Model) -> Vec { - model.iter().collect() - } - - #[test] - fn reorder_inserts_before_target() { - let (mut model, ids) = sample_model(); - assert!(model.reorder(ids[3], ids[1], InsertPosition::Before)); - assert_eq!(order_of(&model), vec![ids[0], ids[3], ids[1], ids[2]]); - } - - #[test] - fn reorder_inserts_after_target() { - let (mut model, ids) = sample_model(); - assert!(model.reorder(ids[0], ids[2], InsertPosition::After)); - assert_eq!(order_of(&model), vec![ids[1], ids[2], ids[0], ids[3]]); - } - - #[test] - fn reorder_rejects_invalid_entities() { - let (mut model, ids) = sample_model(); - assert!(!model.reorder(ids[0], ids[0], InsertPosition::After)); - } -} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 72bc7580..ff614840 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 use super::model::{Entity, Model, Selectable}; -use super::{InsertPosition, ReorderEvent}; use crate::iced_core::id::Internal; use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::dnd_destination::DragId; @@ -13,9 +12,7 @@ use crate::widget::menu::{ use crate::widget::{Icon, icon}; use crate::{Element, Renderer}; use derive_setters::Setters; -use iced::clipboard::dnd::{ - self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent, -}; +use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}; use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ @@ -44,8 +41,6 @@ thread_local! { static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); } -const TAB_REORDER_LOG_TARGET: &str = "libcosmic::widget::tab_reorder"; - /// A command that focuses a segmented item stored in a widget. pub fn focus(id: Id) -> Task { task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0)))) @@ -56,27 +51,6 @@ pub enum ItemBounds { Divider(Rectangle, bool), } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum DropSide { - Before, - After, -} - -impl From for InsertPosition { - fn from(side: DropSide) -> Self { - match side { - DropSide::Before => InsertPosition::Before, - DropSide::After => InsertPosition::After, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct DropHint { - entity: Entity, - side: DropSide, -} - /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { const VERTICAL: bool; @@ -183,12 +157,6 @@ where #[setters(strip_option)] pub(super) drag_id: Option, #[setters(skip)] - pub(super) tab_drag: Option>, - #[setters(skip)] - pub(super) on_drop_hint: Option) -> Message + 'static>>, - #[setters(skip)] - pub(super) on_reorder: Option Message + 'static>>, - #[setters(skip)] /// Defines the implementation of this struct variant: PhantomData, } @@ -236,9 +204,6 @@ where mimes: Vec::new(), variant: PhantomData, drag_id: None, - tab_drag: None, - on_drop_hint: None, - on_reorder: None, } } @@ -296,77 +261,6 @@ where self } - /// Enable drag-and-drop support for tabs using the provided payload builder. - pub fn enable_tab_drag( - mut self, - payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static, - ) -> Self { - self.tab_drag = Some(TabDragSource::new(payload)); - self - } - - /// Receive drop hint updates during drag-and-drop. - pub fn on_drop_hint( - mut self, - callback: impl Fn(Option<(Entity, bool)>) -> Message + 'static, - ) -> Self { - self.on_drop_hint = Some(Box::new(callback)); - self - } - - /// Emit a message when a tab drag is dropped inside this widget. - pub fn on_reorder(mut self, callback: impl Fn(ReorderEvent) -> Message + 'static) -> Self { - self.on_reorder = Some(Box::new(callback)); - self - } - - /// Set the pointer distance threshold before a drag is started. - pub fn tab_drag_threshold(mut self, threshold: f32) -> Self { - if let Some(tab_drag) = self.tab_drag.as_mut() { - tab_drag.threshold = threshold.max(1.0); - } - self - } - - fn reorder_event_for_drop(&self, state: &LocalState, target: Entity) -> Option { - let dragged = state.dragging_tab?; - if dragged == target - || !self.model.contains_item(dragged) - || !self.model.contains_item(target) - { - return None; - } - let position = state - .drop_hint - .filter(|hint| hint.entity == target) - .map(|hint| InsertPosition::from(hint.side)) - .unwrap_or_else(|| self.default_insert_position(dragged, target)); - Some(ReorderEvent { - dragged, - target, - position, - }) - } - - fn default_insert_position(&self, dragged: Entity, target: Entity) -> InsertPosition { - let len = self.model.len(); - let target_pos = self - .model - .position(target) - .map(|pos| pos as usize) - .unwrap_or(len); - let from_pos = self - .model - .position(dragged) - .map(|pos| pos as usize) - .unwrap_or(target_pos); - if from_pos < target_pos { - InsertPosition::After - } else { - InsertPosition::Before - } - } - /// Check if an item is enabled. fn is_enabled(&self, key: Entity) -> bool { self.model.items.get(key).is_some_and(|item| item.enabled) @@ -651,101 +545,6 @@ where state.pressed_item == Some(Item::Tab(key)) } - fn emit_drop_hint(&self, shell: &mut Shell<'_, Message>, hint: Option) { - if let Some(on_hint) = self.on_drop_hint.as_ref() { - let mapped = hint.map(|hint| (hint.entity, matches!(hint.side, DropSide::After))); - shell.publish(on_hint(mapped)); - } - } - - fn drop_hint_for_position( - &self, - state: &LocalState, - bounds: Rectangle, - cursor: Point, - ) -> Option { - let dragging = state.dragging_tab?; - - self.variant_bounds(state, bounds) - .filter_map(|item| match item { - ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)), - _ => None, - }) - .find_map(|(entity, rect)| { - let before = if Self::VERTICAL { - cursor.y < rect.center_y() - } else { - cursor.x < rect.center_x() - }; - Some(DropHint { - entity, - side: if before { - DropSide::Before - } else { - DropSide::After - }, - }) - }) - } - - fn start_tab_drag( - &self, - state: &mut LocalState, - entity: Entity, - bounds: Rectangle, - cursor: Point, - clipboard: &mut dyn Clipboard, - ) -> bool { - let Some(tab_drag) = self.tab_drag.as_ref() else { - return false; - }; - - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "start_tab_drag requested entity={:?} cursor=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - entity, - cursor.x, - cursor.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - - let Some((mime, data)) = (tab_drag.payload)(entity) else { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "start_tab_drag aborted entity={:?}: payload builder returned None", - entity - ); - return false; - }; - - let data_len = data.len(); - let mime_label = mime.clone(); - - iced_core::clipboard::start_dnd::( - clipboard, - false, - Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())), - None, - Box::new(SimpleDragData::new(mime, data)), - DndAction::Move, - ); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag started entity={:?} mime={} bytes={}", - entity, - mime_label, - data_len - ); - state.dragging_tab = Some(entity); - state.tab_drag_candidate = None; - state.pressed_item = None; - true - } - /// Returns the drag id of the destination. /// /// # Panics @@ -812,9 +611,6 @@ where dnd_state: Default::default(), fingers_pressed: Default::default(), pressed_item: None, - tab_drag_candidate: None, - dragging_tab: None, - drop_hint: None, }) } @@ -905,7 +701,7 @@ where layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, - clipboard: &mut dyn Clipboard, + _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, ) -> event::Status { @@ -921,26 +717,7 @@ where .drag_offer .as_ref() .map(|dnd_state| dnd_state.data); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "segmented button {:?} received DnD event: {:?} entity={entity:?}", - my_id, - e - ); match e { - DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => { - if state.dragging_tab.take().is_some() { - state.tab_drag_candidate = None; - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag source finished id={:?}", - my_id - ); - return event::Status::Captured; - } - } DndEvent::Offer( id, OfferEvent::Enter { @@ -955,16 +732,6 @@ where }) .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32))) .map(|(key, _)| key); - state.drop_hint = self.drop_hint_for_position( - state, - bounds, - Point::new(*x as f32, *y as f32), - ); - self.emit_drop_hint(shell, state.drop_hint); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" - ); let on_dnd_enter = self.on_dnd_enter @@ -983,28 +750,15 @@ where ); } DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} - DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) - if Some(my_id) == *id => - { - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); + DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) => { if let Some(Some(entity)) = entity { if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { shell.publish(on_dnd_leave(entity)); } } - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer leave id={my_id:?} entity={entity:?}" - ); _ = state.dnd_state.on_leave::(None); } - DndEvent::Offer(_, OfferEvent::Leave | OfferEvent::LeaveDestination) => {} DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}" - ); let new = self .variant_bounds(state, bounds) .filter_map(|item| match item { @@ -1021,12 +775,6 @@ where None:: Message>, Some(new_entity), ); - state.drop_hint = self.drop_hint_for_position( - state, - bounds, - Point::new(*x as f32, *y as f32), - ); - self.emit_drop_hint(shell, state.drop_hint); if Some(Some(new_entity)) != entity { let prev_action = state .dnd_state @@ -1044,12 +792,6 @@ where } } } else if entity.is_some() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer motion leaving id={my_id:?}" - ); - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); state.dnd_state.on_motion::( *x, *y, @@ -1065,81 +807,32 @@ where } } DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer drop id={my_id:?} entity={entity:?}" - ); _ = state .dnd_state .on_drop::(None:: Message>); } DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => { if state.dnd_state.drag_offer.is_some() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer selected action id={my_id:?} action={action:?} entity={entity:?}" - ); _ = state .dnd_state .on_action_selected::(*action, None:: Message>); } } DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}" - ); - let drop_entity = entity - .flatten() - .or_else(|| state.drop_hint.map(|hint| hint.entity)); - let allow_reorder = state - .dnd_state - .drag_offer - .as_ref() - .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); - let pending_reorder = if allow_reorder && self.on_reorder.is_some() { - drop_entity.and_then(|target| self.reorder_event_for_drop(state, target)) - } else { - None - }; - if let Some(entity) = drop_entity { + if let Some(Some(entity)) = entity { let on_drop = self.on_dnd_drop.as_ref(); let on_drop = on_drop.map(|on_drop| { |mime, data, action, _, _| on_drop(entity, data, mime, action) }); - let (maybe_msg, ret) = state.dnd_state.on_data_received( + if let (Some(msg), ret) = state.dnd_state.on_data_received( mem::take(mime_type), mem::take(data), None:: Message>, on_drop, - ); - if let Some(msg) = maybe_msg { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "publishing drop message entity={entity:?}" - ); + ) { shell.publish(msg); - } - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); - if let Some(event) = pending_reorder { - if let Some(on_reorder) = self.on_reorder.as_ref() { - shell.publish(on_reorder(event)); - } - } - return ret; - } else { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "data received without entity id={my_id:?}" - ); - state.drop_hint = None; - self.emit_drop_hint(shell, state.drop_hint); - if let Some(event) = pending_reorder { - if let Some(on_reorder) = self.on_reorder.as_ref() { - shell.publish(on_reorder(event)); - } + return ret; } } } @@ -1204,16 +897,12 @@ where // Record that the mouse is hovering over this button. state.hovered = Item::Tab(key); - let close_button_bounds = - close_bounds(bounds, f32::from(self.close_icon.size)); - let over_close_button = self.model.items[key].closable - && cursor_position.is_over(close_button_bounds); - // If marked as closable, show a close icon. if self.model.items[key].closable { // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { - if over_close_button + if cursor_position + .is_over(close_bounds(bounds, f32::from(self.close_icon.size))) && (left_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 1)) { @@ -1238,36 +927,6 @@ where } } - if self.tab_drag.is_some() - && matches!( - event, - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - ) - && !over_close_button - { - if let Some(position) = cursor_position.position() { - state.tab_drag_candidate = Some(TabDragCandidate { - entity: key, - bounds, - origin: position, - }); - if let Some(tab_drag) = self.tab_drag.as_ref() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - key, - position.x, - position.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - } - } - } - if is_lifted(&event) { state.unfocus(); } @@ -1387,42 +1046,6 @@ where state.pressed_item = None; } - if let (Some(tab_drag), Some(candidate)) = - (self.tab_drag.as_ref(), state.tab_drag_candidate) - { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { - if let Some(position) = cursor_position.position() { - if position.distance(candidate.origin) >= tab_drag.threshold { - if let Some(candidate) = state.tab_drag_candidate.take() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag threshold met entity={:?} distance={:.2} threshold={}", - candidate.entity, - position.distance(candidate.origin), - tab_drag.threshold - ); - if self.start_tab_drag( - state, - candidate.entity, - candidate.bounds, - position, - clipboard, - ) { - return event::Status::Captured; - } - } - } - } - } - } - - if matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - ) { - state.tab_drag_candidate = None; - } - if state.is_focused() { if let Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Tab), @@ -1497,7 +1120,6 @@ where ) { let state = tree.state.downcast_mut::(); operation.focusable(state, Some(&self.id.0)); - operation.custom(state, Some(&self.id.0)); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { @@ -1558,12 +1180,6 @@ where let appearance = Self::variant_appearance(theme, &self.style); let bounds: Rectangle = layout.bounds(); let button_amount = self.model.items.len(); - let show_drop_hint = state.dragging_tab.is_some(); - let drop_hint = if show_drop_hint { - state.drop_hint - } else { - None - }; // Draw the background, if a background was defined. if let Some(background) = appearance.background { @@ -1689,8 +1305,6 @@ where // Draw each of the items in the widget. let mut nth = 0; - let drop_hint_marker = drop_hint; - let show_drop_hint_marker = show_drop_hint; self.variant_bounds(state, bounds).for_each(move |item| { let (key, mut bounds) = match item { // Draw a button @@ -1718,27 +1332,8 @@ where } }; - let original_bounds = bounds; let center_y = bounds.center_y(); - if show_drop_hint_marker { - if matches!( - drop_hint_marker, - Some(DropHint { - entity, - side: DropSide::Before - }) if entity == key - ) { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::Before, - Self::VERTICAL, - appearance.active.text_color, - ); - } - } - let menu_open = || { state.show_context == Some(key) && !tree.children.is_empty() @@ -1803,6 +1398,7 @@ where ); } + let original_bounds = bounds; bounds.x += f32::from(self.button_padding[0]); bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); let mut indent_padding = 0.0; @@ -2000,24 +1596,6 @@ where ); } - if show_drop_hint_marker { - if matches!( - drop_hint_marker, - Some(DropHint { - entity, - side: DropSide::After - }) if entity == key - ) { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::After, - Self::VERTICAL, - appearance.active.text_color, - ); - } - } - nth += 1; }); } @@ -2081,68 +1659,27 @@ where fn drag_destinations( &self, - tree: &Tree, + _state: &Tree, layout: Layout<'_>, _renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - let local_state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let my_id = self.get_drag_id(); - let mut pushed = false; - - for item in self.variant_bounds(local_state, layout.bounds()) { - if let ItemBounds::Button(_entity, rect) = item { - pushed = true; - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - rect.x, - rect.y, - rect.width, - rect.height, - self.mimes - ); - dnd_rectangles.push(DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(rect.x), - y: f64::from(rect.y), - width: f64::from(rect.width), - height: f64::from(rect.height), - }, - mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), - actions: DndAction::Copy | DndAction::Move, - preferred: DndAction::Move, - }); - } - } - - if !pushed { - let bounds = layout.bounds(); - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", - my_id, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - self.mimes - ); - dnd_rectangles.push(DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(bounds.x), - y: f64::from(bounds.y), - width: f64::from(bounds.width), - height: f64::from(bounds.height), - }, - mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), - actions: DndAction::Copy | DndAction::Move, - preferred: DndAction::Move, - }); - } + let dnd_rect = DndDestinationRectangle { + id: my_id, + rectangle: dnd::Rectangle { + x: f64::from(bounds.x), + y: f64::from(bounds.y), + width: f64::from(bounds.width), + height: f64::from(bounds.height), + }, + mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), + actions: DndAction::Copy | DndAction::Move, + preferred: DndAction::Move, + }; + dnd_rectangles.push(dnd_rect); } } @@ -2164,54 +1701,6 @@ where } } -struct TabDragSource { - payload: Box Option<(String, Vec)>>, - threshold: f32, - _marker: PhantomData, -} - -impl TabDragSource { - fn new(payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static) -> Self { - Self { - payload: Box::new(payload), - threshold: 8.0, - _marker: PhantomData, - } - } -} - -struct SimpleDragData { - mime: String, - bytes: Vec, -} - -impl SimpleDragData { - fn new(mime: String, bytes: Vec) -> Self { - Self { mime, bytes } - } -} - -impl iced::clipboard::mime::AsMimeTypes for SimpleDragData { - fn available(&self) -> Cow<'static, [String]> { - Cow::Owned(vec![self.mime.clone()]) - } - - fn as_bytes(&self, mime_type: &str) -> Option> { - if mime_type == self.mime { - Some(Cow::Owned(self.bytes.clone())) - } else { - None - } - } -} - -#[derive(Clone, Copy)] -struct TabDragCandidate { - entity: Entity, - bounds: Rectangle, - origin: Point, -} - #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, @@ -2258,12 +1747,6 @@ pub struct LocalState { fingers_pressed: HashSet, /// The currently pressed item pressed_item: Option, - /// Pending tab drag candidate data - tab_drag_candidate: Option, - /// Currently dragging tab entity - dragging_tab: Option, - /// Current drop hint for drag-and-drop indicator - drop_hint: Option, } #[derive(Debug, Default, PartialEq)] @@ -2288,143 +1771,6 @@ impl LocalState { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::widget::segmented_button::{self, Appearance as SegAppearance}; - use iced::Size; - use slotmap::SecondaryMap; - use std::collections::HashSet; - - #[derive(Clone, Debug)] - enum TestMessage {} - - struct TestVariant; - - impl SegmentedVariant - for SegmentedButton<'_, TestVariant, SelectionMode, Message> - where - Model: Selectable, - SelectionMode: Default, - { - const VERTICAL: bool = false; - - fn variant_appearance( - _theme: &crate::Theme, - _style: &crate::theme::SegmentedButton, - ) -> SegAppearance { - SegAppearance::default() - } - - fn variant_bounds<'b>( - &'b self, - _state: &'b LocalState, - bounds: Rectangle, - ) -> Box + 'b> { - let len = self.model.order.len(); - if len == 0 { - return Box::new(std::iter::empty()); - } - let width = bounds.width / len as f32; - Box::new( - self.model - .order - .iter() - .copied() - .enumerate() - .map(move |(idx, entity)| { - let rect = Rectangle { - x: bounds.x + (idx as f32) * width, - y: bounds.y, - width, - height: bounds.height, - }; - ItemBounds::Button(entity, rect) - }), - ) - } - - fn variant_layout( - &self, - _state: &mut LocalState, - _renderer: &crate::Renderer, - _limits: &layout::Limits, - ) -> Size { - Size::ZERO - } - } - - fn sample_model() -> ( - segmented_button::SingleSelectModel, - Vec, - ) { - let mut entities = Vec::new(); - let model = segmented_button::Model::builder() - .insert(|b| b.text("One").with_id(|id| entities.push(id))) - .insert(|b| b.text("Two").with_id(|id| entities.push(id))) - .insert(|b| b.text("Three").with_id(|id| entities.push(id))) - .build(); - (model, entities) - } - - fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState { - let mut state = LocalState { - menu_state: MenuBarState::default(), - paragraphs: SecondaryMap::new(), - text_hashes: SecondaryMap::new(), - buttons_visible: 0, - buttons_offset: 0, - collapsed: false, - focused: None, - focused_item: Item::default(), - focused_visible: false, - hovered: Item::default(), - known_length: 0, - middle_clicked: None, - internal_layout: Vec::new(), - context_cursor: Point::ORIGIN, - show_context: None, - wheel_timestamp: None, - dnd_state: crate::widget::dnd_destination::State::>::new(), - fingers_pressed: HashSet::new(), - pressed_item: None, - tab_drag_candidate: None, - dragging_tab: Some(dragging), - drop_hint: None, - }; - state.buttons_visible = len; - state.known_length = len; - state - } - - #[test] - fn drop_hint_reports_before_and_after() { - let (model, ids) = sample_model(); - let button = - SegmentedButton::::new( - &model, - ); - let state = test_state(ids[0], model.order.len()); - let bounds = Rectangle { - x: 0.0, - y: 0.0, - width: 300.0, - height: 30.0, - }; - let before = button - .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0)) - .expect("hint"); - assert_eq!(before.entity, ids[0]); - assert!(matches!(before.side, DropSide::Before)); - - let after = button - .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0)) - .expect("hint"); - assert_eq!(after.entity, ids[2]); - assert!(matches!(after.side, DropSide::After)); - } -} - impl operation::Focusable for LocalState { fn is_focused(&self) -> bool { self.focused @@ -2537,53 +1883,6 @@ fn draw_icon( ); } -fn draw_drop_indicator( - renderer: &mut Renderer, - bounds: Rectangle, - side: DropSide, - vertical: bool, - color: Color, -) { - let thickness = 4.0; - let quad_bounds = if vertical { - let y = match side { - DropSide::Before => bounds.y - thickness / 2.0, - DropSide::After => bounds.y + bounds.height - thickness / 2.0, - }; - - Rectangle { - x: bounds.x, - y, - width: bounds.width, - height: thickness, - } - } else { - let x = match side { - DropSide::Before => bounds.x - thickness / 2.0, - DropSide::After => bounds.x + bounds.width - thickness / 2.0, - }; - - Rectangle { - x, - y: bounds.y, - width: thickness, - height: bounds.height, - } - }; - - renderer.fill_quad( - renderer::Quad { - bounds: quad_bounds, - border: Border { - radius: 2.0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - }, - Background::Color(color), - ); -} - fn left_button_released(event: &Event) -> bool { matches!( event, From dd3610b8ae4f1bcf2e2299e82f908913d1a4a57d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Dec 2025 15:35:21 -0500 Subject: [PATCH 003/168] fix(dnd_destination): layout for dnd rectangle children --- src/widget/dnd_destination.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index c943d2c7..947d2fe3 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -597,14 +597,12 @@ impl Widget }; dnd_rectangles.push(my_dest); - if let Some(child_layout) = layout.children().next() { - self.container.as_widget().drag_destinations( - &state.children[0], - child_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } + self.container.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); } fn id(&self) -> Option { From 6f92465fcbed24beca85dec3d7e89db6e63a46de Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Dec 2025 12:08:02 +0100 Subject: [PATCH 004/168] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amadɣas Co-authored-by: Hosted Weblate Co-authored-by: Walter William Beckerleg Bruckman Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt_BR/ Translation: Pop OS/libcosmic --- i18n/kab/libcosmic.ftl | 0 i18n/pt-BR/libcosmic.ftl | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 i18n/kab/libcosmic.ftl diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl index f02828bf..51b5f6c3 100644 --- a/i18n/pt-BR/libcosmic.ftl +++ b/i18n/pt-BR/libcosmic.ftl @@ -8,18 +8,18 @@ designers = Designers artists = Artistas translators = Tradutores documenters = Documentadores -january = Janeiro { $year } -february = Fevereiro { $year } -march = Março { $year } -april = Abril { $year } -may = Maio { $year } -june = Junho { $year } -july = Julho { $year } -august = Agosto { $year } -september = Setembro { $year } -october = Outubro { $year } -november = Novembro { $year } -december = Dezembro { $year } +january = Janeiro de { $year } +february = Fevereiro de { $year } +march = Março de { $year } +april = Abril de { $year } +may = Maio de { $year } +june = Junho de { $year } +july = Julho de { $year } +august = Agosto de { $year } +september = Setembro de { $year } +october = Outubro de { $year } +november = Novembro de { $year } +december = Dezembro de { $year } monday = Seg tuesday = Ter wednesday = Qua From a9f64c33ce9159485be5dad1ce07ccf7c12399d5 Mon Sep 17 00:00:00 2001 From: Michael Murphy Date: Tue, 30 Dec 2025 11:53:22 +0100 Subject: [PATCH 005/168] i18n: removing translation for Frankish --- i18n/frk/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 i18n/frk/libcosmic.ftl diff --git a/i18n/frk/libcosmic.ftl b/i18n/frk/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 From e9bb5ed97d9120872e1e28d16f7bfcc3a4b81e2c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 6 Jan 2026 02:25:11 +0100 Subject: [PATCH 006/168] chore: update freedesktop-desktop-entry --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 927444e8..decdac93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,7 +149,7 @@ zbus = { version = "5.11.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } -freedesktop-desktop-entry = { version = "0.7.14", optional = true } +freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } [target.'cfg(not(unix))'.dependencies] From 421552dea1c06e876d5999333794b9ee918340a1 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 6 Jan 2026 02:25:46 +0100 Subject: [PATCH 007/168] fix!(desktop): IconSourceExt::as_cosmic_icon should return Handle with SVG preference --- src/desktop.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 01698af5..0d3dbb52 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -7,23 +7,22 @@ use std::path::{Path, PathBuf}; use std::{borrow::Cow, collections::HashSet, ffi::OsStr}; pub trait IconSourceExt { - fn as_cosmic_icon(&self) -> crate::widget::icon::Icon; + fn as_cosmic_icon(&self) -> crate::widget::icon::Handle; } #[cfg(not(windows))] impl IconSourceExt for fde::IconSource { - fn as_cosmic_icon(&self) -> crate::widget::icon::Icon { + fn as_cosmic_icon(&self) -> crate::widget::icon::Handle { match self { fde::IconSource::Name(name) => crate::widget::icon::from_name(name.as_str()) + .prefer_svg(true) .size(128) .fallback(Some(crate::widget::icon::IconFallback::Names(vec![ "application-default".into(), "application-x-executable".into(), ]))) - .into(), - fde::IconSource::Path(path) => { - crate::widget::icon(crate::widget::icon::from_path(path.clone())) - } + .handle(), + fde::IconSource::Path(path) => crate::widget::icon::from_path(path.clone()), } } } From f6039597b72d3eefe2ee1d6528a04077982db238 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 2 Jan 2026 23:01:52 +0100 Subject: [PATCH 008/168] i18n: translation updates from weblate Co-authored-by: Hosted Weblate Co-authored-by: Walter William Beckerleg Bruckman Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/zh-Hans/libcosmic.ftl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index e7c83e5c..5d9fbd66 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -4,3 +4,23 @@ links = 链接 developers = 开发者 designers = 设计师 translators = 译者 +january = { $year }年1月 +february = { $year }年2月 +march = { $year }年3月 +april = { $year }年4月 +may = { $year }年5月 +june = { $year }年6月 +july = { $year }年7月 +august = { $year }年8月 +september = { $year }年9月 +october = { $year }年10月 +november = { $year }年11月 +december = { $year }年12月 +monday = 周一 +tuesday = 周二 +wednesday = 周三 +thursday = 周四 +friday = 周五 +saturday = 周六 +sunday = 周日 +artists = 艺术家 From b9c24d24212a865977db4871efc13ff890055648 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 9 Jan 2026 23:03:09 +0100 Subject: [PATCH 009/168] feat(a11y): screen reader name and description support for button widgets --- src/widget/button/icon.rs | 11 +++++- src/widget/button/image.rs | 16 ++++++-- src/widget/button/link.rs | 15 ++++++- src/widget/button/mod.rs | 10 +++++ src/widget/button/text.rs | 8 +++- src/widget/spin_button.rs | 81 ++++++++++++++++++++++++++++++++------ 6 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 754bc433..edb54272 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -38,6 +38,10 @@ impl Button<'_, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -151,7 +155,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes ); } - let button = if builder.variant.vertical { + let mut button = if builder.variant.vertical { crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) @@ -167,6 +171,11 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .apply(super::custom) }; + #[cfg(feature = "a11y")] + { + button = button.name(builder.name).description(builder.description); + } + let button = button .padding(0) .id(builder.id) diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 6a5c47b1..ab51e667 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -33,6 +33,10 @@ impl<'a, Message> Button<'a, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -79,12 +83,18 @@ where .width(builder.width) .height(builder.height); - super::custom_image_button(content, builder.variant.on_remove) + let mut button = super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) .on_press_maybe(builder.on_press) - .class(builder.class) - .into() + .class(builder.class); + + #[cfg(feature = "a11y")] + { + button = button.name(builder.name).description(builder.description); + } + + button.into() } } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index b86ef1a3..9ce81268 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -34,6 +34,10 @@ impl<'a, Message> Button<'a, Message> { Self { id: Id::unique(), label: label.into(), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -62,7 +66,7 @@ pub fn icon() -> Handle { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let button: super::Button<'a, Message> = row::with_capacity(2) + let mut button: super::Button<'a, Message> = row::with_capacity(2) .push({ // TODO: Avoid allocation crate::widget::text(builder.label.to_string()) @@ -89,6 +93,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .on_press_maybe(builder.on_press.take()) .class(builder.class); + #[cfg(feature = "a11y")] + { + if !builder.label.is_empty() { + button = button.name(builder.label); + } + + button = button.description(builder.description); + } + if builder.tooltip.is_empty() { button.into() } else { diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index d9a4df94..f5975d39 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -69,6 +69,16 @@ pub struct Builder<'a, Message, Variant> { #[setters(into)] label: Cow<'a, str>, + /// A name for screen reader support + #[cfg(feature = "a11y")] + #[setters(into)] + name: Cow<'a, str>, + + /// A description for screen reader support + #[cfg(feature = "a11y")] + #[setters(into)] + description: Cow<'a, str>, + // Adds a tooltip to the button. #[setters(into)] tooltip: Cow<'a, str>, diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 3f58c932..bcdd02ba 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -63,6 +63,10 @@ impl Button<'_, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -136,8 +140,10 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes #[cfg(feature = "a11y")] { if !builder.label.is_empty() { - button = button.name(builder.label); + button = button.name(builder.label) } + + button = button.description(builder.description); } if builder.tooltip.is_empty() { diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 6f4a4de2..db90a000 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -16,6 +16,7 @@ use std::ops::{Add, Sub}; /// Horizontal spin button widget. pub fn spin_button<'a, T, M>( label: impl Into>, + #[cfg(feature = "a11y")] name: impl Into>, value: T, step: T, min: T, @@ -25,7 +26,7 @@ pub fn spin_button<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - SpinButton::new( + let mut button = SpinButton::new( label, value, step, @@ -33,12 +34,20 @@ where max, Orientation::Horizontal, on_press, - ) + ); + + #[cfg(feature = "a11y")] + { + button = button.name(name.into()); + } + + button } /// Vertical spin button widget. pub fn vertical<'a, T, M>( label: impl Into>, + #[cfg(feature = "a11y")] name: impl Into>, value: T, step: T, min: T, @@ -48,15 +57,22 @@ pub fn vertical<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - SpinButton::new( + let mut button = SpinButton::new( label, value, step, min, max, - Orientation::Vertical, + Orientation::Horizontal, on_press, - ) + ); + + #[cfg(feature = "a11y")] + { + button = button.name(name.into()); + } + + button } #[derive(Clone, Copy)] @@ -71,6 +87,9 @@ where { /// The formatted value of the spin button. label: Cow<'a, str>, + /// A name for screen reader support. + #[cfg(feature = "a11y")] + name: Cow<'a, str>, /// The current value of the spin button. value: T, /// The amount to increment or decrement the value. @@ -99,6 +118,8 @@ where ) -> Self { Self { label: label.into(), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), step, value: if value < min { min @@ -113,6 +134,12 @@ where on_press: Box::from(on_press), } } + + #[cfg(feature = "a11y")] + pub(self) fn name(mut self, name: Cow<'a, str>) -> Self { + self.name = name; + self + } } fn increment(value: T, step: T, _min: T, max: T) -> T @@ -153,21 +180,28 @@ where fn make_button<'a, T, Message>( spin_button: &SpinButton<'a, T, Message>, icon: &'static str, + #[cfg(feature = "a11y")] name: String, operation: fn(T, T, T, T) -> T, ) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - icon::from_name(icon) + let mut button = icon::from_name(icon) .apply(button::icon) .on_press((spin_button.on_press)(operation( spin_button.value, spin_button.step, spin_button.min, spin_button.max, - ))) - .into() + ))); + + #[cfg(feature = "a11y")] + { + button = button.name(name.clone()); + } + + button.into() } fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> @@ -175,9 +209,20 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button(&spin_button, "list-add-symbolic", increment); - + let decrement_button = make_button( + &spin_button, + "list-remove-symbolic", + #[cfg(feature = "a11y")] + [&spin_button.name, " decrease"].concat(), + decrement, + ); + let increment_button = make_button( + &spin_button, + "list-add-symbolic", + #[cfg(feature = "a11y")] + [&spin_button.name, " increase"].concat(), + increment, + ); let label = text::body(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) @@ -198,8 +243,18 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button(&spin_button, "list-add-symbolic", increment); + let decrement_button = make_button( + &spin_button, + "list-remove-symbolic", + [&spin_button.label, " decrease"].concat(), + decrement, + ); + let increment_button = make_button( + &spin_button, + "list-add-symbolic", + [&spin_button.label, " increase"].concat(), + increment, + ); let label = text::body(spin_button.label) .apply(container) From f453db2425fa80d3be65840f490a6f13cf66af98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Miku=C5=82a?= Date: Mon, 12 Jan 2026 21:15:14 +0100 Subject: [PATCH 010/168] chore: update iced submodule This pulls in the fix made in https://github.com/pop-os/iced/pull/253. --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 10db38f9..176589f6 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 10db38f982001a714bd94e99a082368762b378ee +Subproject commit 176589f64cc9adc3cb65da373d2e56c998326fc2 From f00043369074fc5c9528f16ebf32f5aa06896936 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 13 Jan 2026 17:01:27 +0100 Subject: [PATCH 011/168] fix(spin_button): compiler error on build without a11y --- src/widget/spin_button.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index db90a000..13cc881f 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -246,12 +246,14 @@ where let decrement_button = make_button( &spin_button, "list-remove-symbolic", + #[cfg(feature = "a11y")] [&spin_button.label, " decrease"].concat(), decrement, ); let increment_button = make_button( &spin_button, "list-add-symbolic", + #[cfg(feature = "a11y")] [&spin_button.label, " increase"].concat(), increment, ); From b0cbb54bf2b3528c895f7636c7ad1fd520fd2a9e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 13 Jan 2026 17:01:57 +0100 Subject: [PATCH 012/168] chore(widget): remove unused RcWrapper method --- src/widget/wrapper.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 92f26fd4..59c0a376 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -58,14 +58,6 @@ impl RcWrapper { let my_refmut: &mut T = &mut RefCell::borrow_mut(self.data.as_ref()); f(my_refmut) } - - /// # Panics - /// - /// Will panic if used outside of original thread. - pub(crate) unsafe fn as_ptr(&self) -> *mut T { - assert_eq!(self.thread_id, thread::current().id()); - RefCell::as_ptr(self.data.as_ref()) - } } #[derive(Clone)] From 03c440b97a401177ee353ec4b100e56ca80518ba Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 14 Jan 2026 18:46:53 +0100 Subject: [PATCH 013/168] chore(cargo): update all crate dependencies --- Cargo.toml | 30 +++++------ cosmic-config/Cargo.toml | 8 +-- cosmic-theme/Cargo.toml | 6 +-- examples/about/Cargo.toml | 2 +- examples/applet/Cargo.toml | 4 +- examples/application/Cargo.toml | 4 +- examples/calendar/Cargo.toml | 2 +- examples/context-menu/Cargo.toml | 4 +- examples/cosmic/Cargo.toml | 4 +- examples/image-button/Cargo.toml | 4 +- examples/menu/Cargo.toml | 4 +- examples/nav-context/Cargo.toml | 4 +- examples/open-dialog/Cargo.toml | 8 +-- examples/subscriptions/Cargo.toml | 10 ++++ examples/subscriptions/src/main.rs | 80 ++++++++++++++++++++++++++++++ examples/table-view/Cargo.toml | 4 +- examples/text-input/Cargo.toml | 4 +- 17 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 examples/subscriptions/Cargo.toml create mode 100644 examples/subscriptions/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index decdac93..46091bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,8 +99,8 @@ async-std = [ [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.0", default-features = false, optional = true } -async-fs = { version = "2.1", optional = true } +ashpd = { version = "0.12.1", default-features = false, optional = true } +async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.7" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } @@ -113,15 +113,15 @@ i18n-embed = { version = "0.16.0", features = [ "desktop-requester", ] } i18n-embed-fl = "0.10" -rust-embed = "8.7.2" +rust-embed = "8.11.0" css-color = "0.2.8" derive_setters = "0.1.8" futures = "0.3" -image = { version = "0.25.8", default-features = false, features = [ +image = { version = "0.25.9", default-features = false, features = [ "jpeg", "png", ] } -libc = { version = "0.2.175", optional = true } +libc = { version = "0.2.180", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" @@ -130,22 +130,22 @@ rfd = { version = "0.15.4", default-features = false, features = [ "xdg-portal", ], optional = true } rustix = { version = "1.1", features = ["pipe", "process"], optional = true } -serde = { version = "1.0.219", features = ["derive"] } -slotmap = "1.0.7" +serde = { version = "1.0.228", features = ["derive"] } +slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } -thiserror = "2.0.16" -taffy = { version = "0.9.1", features = ["grid"] } -tokio = { version = "1.47.1", optional = true } -tracing = "0.1.41" +thiserror = "2.0.17" +taffy = { version = "0.9.2", features = ["grid"] } +tokio = { version = "1.49.0", optional = true } +tracing = "0.1.44" unicode-segmentation = "1.12" -url = "2.5.7" -zbus = { version = "5.11.0", default-features = false, optional = true } +url = "2.5.8" +zbus = { version = "5.13.1", default-features = false, optional = true } # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus = { version = "5.11.0", default-features = false } +zbus = { version = "5.13.1", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -225,4 +225,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.13.0" +tempfile = "3.24.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 9b5aca07..78d671ca 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,18 +11,18 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.11.0", default-features = false, optional = true } +zbus = { version = "5.13.1", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.3", optional = true } notify = "8.2.0" ron = "0.11.0" -serde = "1.0.219" +serde = "1.0.228" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } futures-util = { version = "0.3", optional = true } dirs.workspace = true -tokio = { version = "1.47", optional = true, features = ["time"] } +tokio = { version = "1.49", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" @@ -30,4 +30,4 @@ tracing = "0.1" xdg = "3.0" [target.'cfg(windows)'.dependencies] -known-folders = "1.3.1" +known-folders = "1.4.0" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 44b0df5a..10b548b4 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -17,8 +17,8 @@ no-default = [] [dependencies] palette = { version = "0.7.6", features = ["serializing"] } almost = "0.2" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = { version = "1.0.143", optional = true, features = [ +serde = { version = "1.0.228", features = ["derive"] } +serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } ron = "0.11.0" @@ -28,4 +28,4 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features "macro", ] } dirs.workspace = true -thiserror = "2.0.16" +thiserror = "2.0.17" diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index d2642cd6..f980811c 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -open = "5.3.2" +open = "5.3.3" [dependencies.libcosmic] path = "../../" diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index c39ca288..f97bff44 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" [dependencies] once_cell = "1" -rust-embed = "8.6.0" +rust-embed = "8.11.0" tracing = "0.1" env_logger = "0.10.2" -log = "0.4.26" +log = "0.4.29" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 28c13117..f05c0418 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,8 +8,8 @@ default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index 9ffb838c..59b23c0c 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = "0.4.40" +chrono = "0.4.42" [dependencies.libcosmic] path = "../../" diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 45cbf78a..39c550f4 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 695f0c37..8c2a3126 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -19,9 +19,9 @@ libcosmic = { path = "../..", features = [ "xdg-portal", ] } once_cell = "1.21" -slotmap = "1.0.7" +slotmap = "1.1.1" env_logger = "0.10" -log = "0.4.26" +log = "0.4.29" [dependencies.cosmic-time] git = "https://github.com/pop-os/cosmic-time" diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml index cf61955a..c219a53b 100644 --- a/examples/image-button/Cargo.toml +++ b/examples/image-button/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" [dependencies.libcosmic] path = "../../" diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml index dcab1ef5..430b26ea 100644 --- a/examples/menu/Cargo.toml +++ b/examples/menu/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml index 93dbe3e9..d829df0f 100644 --- a/examples/nav-context/Cargo.toml +++ b/examples/nav-context/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index 2a734da0..94049270 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -10,10 +10,10 @@ xdg-portal = ["libcosmic/xdg-portal"] [dependencies] apply = "0.3.0" -tokio = { version = "1.44", features = ["full"] } -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -url = "2.5.4" +tokio = { version = "1.49", features = ["full"] } +tracing = "0.1.44" +tracing-subscriber = "0.3.22" +url = "2.5.8" [dependencies.libcosmic] features = ["debug", "winit", "wgpu", "wayland", "tokio"] diff --git a/examples/subscriptions/Cargo.toml b/examples/subscriptions/Cargo.toml new file mode 100644 index 00000000..8eb69ff3 --- /dev/null +++ b/examples/subscriptions/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "subscriptions" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[dependencies.libcosmic] +path = "../../" +features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs new file mode 100644 index 00000000..47bd3772 --- /dev/null +++ b/examples/subscriptions/src/main.rs @@ -0,0 +1,80 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Application API example + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::Subscription; +use cosmic::{executor, prelude::*, widget}; + +/// Runs application with these settings +fn main() -> Result<(), Box> { + cosmic::app::run::(Settings::default(), ())?; + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message {} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = (); + + /// Message type specific to our [`App`]. + type Message = Message; + + /// The unique application ID to supply to the window manager. + const APP_ID: &'static str = "org.cosmic.TextInputsDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits task on initialize. + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { + let mut app = App { core }; + + let commands = Task::batch(vec![app.update_title()]); + + (app, commands) + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Task { + Task::none() + } + + /// Creates a view after each update. + fn view(&self) -> Element<'_, Self::Message> { + widget::row().into() + } +} + +impl App +where + Self: cosmic::Application, +{ + fn update_title(&mut self) -> Task { + let window_title = format!("COSMIC Subscriptions Demo"); + self.set_header_title(window_title.clone()); + self.set_window_title(window_title, self.core.main_window_id().unwrap()) + } +} diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml index 41669cb8..8ed45928 100644 --- a/examples/table-view/Cargo.toml +++ b/examples/table-view/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.37" -tracing-subscriber = "0.3.17" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" chrono = "*" diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml index fb1bdf28..fe6105c2 100644 --- a/examples/text-input/Cargo.toml +++ b/examples/text-input/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] From 85709b5c2943648df1293baeef852dc0d7907d2e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 15 Jan 2026 15:23:51 +0100 Subject: [PATCH 014/168] fix(iced): fix for crash in cosmic-launcher --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 176589f6..2db5545f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 176589f64cc9adc3cb65da373d2e56c998326fc2 +Subproject commit 2db5545fbee505c2c643c628a8984d1666c4d451 From 3e6c9a6addca2dfe8cadc1dbd03add72cb6d0673 Mon Sep 17 00:00:00 2001 From: Jonatan Pettersson Date: Fri, 16 Jan 2026 14:19:06 +0100 Subject: [PATCH 015/168] feat: add optional placeholder text to dropdown --- src/widget/dropdown/widget.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index d4a9bc87..03be4eb3 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -47,6 +47,8 @@ where gap: f32, #[setters(into)] padding: Padding, + #[setters(strip_option, into)] + placeholder: Option>, #[setters(strip_option)] text_size: Option, text_line_height: text::LineHeight, @@ -86,6 +88,7 @@ where selections, icons: Cow::Borrowed(&[]), selected, + placeholder: None, width: Length::Shrink, gap: Self::DEFAULT_GAP, padding: Self::DEFAULT_PADDING, @@ -115,6 +118,7 @@ where selections, icons, selected, + placeholder, width, gap, padding, @@ -131,6 +135,7 @@ where selections, icons, selected, + placeholder, width, gap, padding, @@ -241,6 +246,7 @@ where .map(AsRef::as_ref) .zip(tree.state.downcast_mut::().selections.get_mut(id)) }), + self.placeholder.as_deref(), !self.icons.is_empty(), ) } @@ -313,6 +319,7 @@ where font, self.selected.and_then(|id| self.selections.get(id)), self.selected.and_then(|id| self.icons.get(id)), + self.placeholder.as_deref(), tree.state.downcast_ref::(), viewport, ); @@ -451,6 +458,7 @@ pub fn layout( text_line_height: text::LineHeight, font: Option, selection: Option<(&str, &mut crate::Plain)>, + placeholder: Option<&str>, has_icons: bool, ) -> layout::Node { use std::f32; @@ -459,8 +467,8 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 { - paragraph.update(Text { + let measure = move |(label, paragraph): (_, Option<&mut crate::Plain>)| -> f32 { + let text = Text { content: label, bounds: Size::new(f32::MAX, f32::MAX), size: iced::Pixels(text_size), @@ -470,11 +478,22 @@ pub fn layout( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), - }); + }; + let paragraph = match paragraph { + Some(p) => { + p.update(text); + p + } + None => &mut crate::Plain::new(text), + }; paragraph.min_width().round() }; - selection.map(measure).unwrap_or_default() + selection + .map(|(l, p)| (l, Some(p))) + .or_else(|| placeholder.map(|l| (l, None))) + .map(measure) + .unwrap_or_default() } _ => 0.0, }; @@ -841,6 +860,7 @@ pub fn draw<'a, S>( font: crate::font::Font, selected: Option<&'a S>, icon: Option<&'a icon::Handle>, + placeholder: Option<&'a str>, state: &'a State, viewport: &Rectangle, ) where @@ -880,7 +900,7 @@ pub fn draw<'a, S>( ); } - if let Some(content) = selected.map(AsRef::as_ref) { + if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); let mut bounds = Rectangle { From 097c76f0e56919f4c168e8a53aa5e67e207ac8b1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 16 Jan 2026 17:23:40 +0100 Subject: [PATCH 016/168] i18n: translation updates from weblate Co-authored-by: Baurzhan Muftakhidinov Co-authored-by: Hosted Weblate --- i18n/kk/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/kk/libcosmic.ftl diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 689f25be539bb7163fe01dd3daaa253dc212f131 Mon Sep 17 00:00:00 2001 From: vacenty <193441458+vacenty@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:08:25 +0100 Subject: [PATCH 017/168] feat(spin_button): when value is min/maxed, disable decrease/increase button --- src/widget/spin_button.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 13cc881f..9ad81b4d 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -181,20 +181,22 @@ fn make_button<'a, T, Message>( spin_button: &SpinButton<'a, T, Message>, icon: &'static str, #[cfg(feature = "a11y")] name: String, - operation: fn(T, T, T, T) -> T, + operation: Option T>, ) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let mut button = icon::from_name(icon) - .apply(button::icon) - .on_press((spin_button.on_press)(operation( + let mut button = icon::from_name(icon).apply(button::icon); + + if let Some(f) = operation { + button = button.on_press((spin_button.on_press)(f( spin_button.value, spin_button.step, spin_button.min, spin_button.max, - ))); + ))) + }; #[cfg(feature = "a11y")] { @@ -214,14 +216,20 @@ where "list-remove-symbolic", #[cfg(feature = "a11y")] [&spin_button.name, " decrease"].concat(), - decrement, + match spin_button.value == spin_button.min { + true => None, + false => Some(decrement), + }, ); let increment_button = make_button( &spin_button, "list-add-symbolic", #[cfg(feature = "a11y")] [&spin_button.name, " increase"].concat(), - increment, + match spin_button.value == spin_button.max { + true => None, + false => Some(increment), + }, ); let label = text::body(spin_button.label) .apply(container) @@ -248,14 +256,20 @@ where "list-remove-symbolic", #[cfg(feature = "a11y")] [&spin_button.label, " decrease"].concat(), - decrement, + match spin_button.value == spin_button.min { + true => None, + false => Some(decrement), + }, ); let increment_button = make_button( &spin_button, "list-add-symbolic", #[cfg(feature = "a11y")] [&spin_button.label, " increase"].concat(), - increment, + match spin_button.value == spin_button.max { + true => None, + false => Some(increment), + }, ); let label = text::body(spin_button.label) From d71c42102d9899d8a6a924c4b064175d2e4a2230 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 21 Jan 2026 19:21:46 -0500 Subject: [PATCH 018/168] fix(segmented button): tab dnd --- src/widget/segmented_button/widget.rs | 117 +++++++++++++++++--------- 1 file changed, 75 insertions(+), 42 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 72bc7580..7a01749e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -297,11 +297,8 @@ where } /// Enable drag-and-drop support for tabs using the provided payload builder. - pub fn enable_tab_drag( - mut self, - payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static, - ) -> Self { - self.tab_drag = Some(TabDragSource::new(payload)); + pub fn enable_tab_drag(mut self, mime: String) -> Self { + self.tab_drag = Some(TabDragSource::new(mime)); self } @@ -664,28 +661,29 @@ where bounds: Rectangle, cursor: Point, ) -> Option { - let dragging = state.dragging_tab?; + let _ = state.dragging_tab?; self.variant_bounds(state, bounds) .filter_map(|item| match item { ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)), _ => None, }) - .find_map(|(entity, rect)| { + .map(|(entity, rect)| { let before = if Self::VERTICAL { cursor.y < rect.center_y() } else { cursor.x < rect.center_x() }; - Some(DropHint { + DropHint { entity, side: if before { DropSide::Before } else { DropSide::After }, - }) + } }) + .next() } fn start_tab_drag( @@ -713,33 +711,24 @@ where tab_drag.threshold ); - let Some((mime, data)) = (tab_drag.payload)(entity) else { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "start_tab_drag aborted entity={:?}: payload builder returned None", - entity - ); - return false; - }; - - let data_len = data.len(); - let mime_label = mime.clone(); + let data_len = 0; iced_core::clipboard::start_dnd::( clipboard, false, Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())), None, - Box::new(SimpleDragData::new(mime, data)), + Box::new(SimpleDragData::new(tab_drag.mime.clone(), vec![1])), DndAction::Move, ); log::trace!( target: TAB_REORDER_LOG_TARGET, "tab drag started entity={:?} mime={} bytes={}", entity, - mime_label, + tab_drag.mime, data_len ); + state.dragging_tab = Some(entity); state.tab_drag_candidate = None; state.pressed_item = None; @@ -815,6 +804,7 @@ where tab_drag_candidate: None, dragging_tab: None, drop_hint: None, + offer_mimes: Vec::new(), }) } @@ -966,26 +956,29 @@ where "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" ); - let on_dnd_enter = - self.on_dnd_enter - .as_ref() - .zip(entity) - .map(|(on_enter, entity)| { - move |_, _, mime_types| on_enter(entity, mime_types) - }); + let on_dnd_enter = self + .on_dnd_enter + .as_ref() + .zip(entity) + .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes)); + let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) + && mime_types.is_empty() + { + vec![mime.clone()] + } else { + mime_types.clone() + }; + state.offer_mimes = mimes.clone(); - _ = state.dnd_state.on_enter::( - *x, - *y, - mime_types.clone(), - on_dnd_enter, - entity, - ); + _ = state + .dnd_state + .on_enter::(*x, *y, mimes, on_dnd_enter, entity); } DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) if Some(my_id) == *id => { + state.dragging_tab = None; state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { @@ -999,7 +992,6 @@ where ); _ = state.dnd_state.on_leave::(None); } - DndEvent::Offer(_, OfferEvent::Leave | OfferEvent::LeaveDestination) => {} DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { log::trace!( target: TAB_REORDER_LOG_TARGET, @@ -1034,7 +1026,7 @@ where .as_ref() .map(|dnd| dnd.selected_action); if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() { - shell.publish(on_dnd_enter(new_entity, Vec::new())); + shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone())); } if let Some(dnd) = state.dnd_state.drag_offer.as_mut() { dnd.data = Some(new_entity); @@ -1097,7 +1089,11 @@ where .drag_offer .as_ref() .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); - let pending_reorder = if allow_reorder && self.on_reorder.is_some() { + let pending_reorder = if allow_reorder + && self.on_reorder.is_some() + && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type) + && state.dragging_tab.is_some() + { drop_entity.and_then(|target| self.reorder_event_for_drop(state, target)) } else { None @@ -1122,6 +1118,8 @@ where shell.publish(msg); } state.drop_hint = None; + state.dragging_tab = None; + self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { if let Some(on_reorder) = self.on_reorder.as_ref() { @@ -1135,6 +1133,8 @@ where "data received without entity id={my_id:?}" ); state.drop_hint = None; + state.dragging_tab = None; + self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { if let Some(on_reorder) = self.on_reorder.as_ref() { @@ -2118,6 +2118,36 @@ where } } + if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) { + for item in self.variant_bounds(local_state, layout.bounds()) { + if let ItemBounds::Button(_entity, rect) = item { + pushed = true; + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", + my_id, + rect.x, + rect.y, + rect.width, + rect.height, + mime + ); + dnd_rectangles.push(DndDestinationRectangle { + id: my_id, + rectangle: dnd::Rectangle { + x: f64::from(rect.x), + y: f64::from(rect.y), + width: f64::from(rect.width), + height: f64::from(rect.height), + }, + mime_types: vec![Cow::Owned(mime.clone())], + actions: DndAction::Copy | DndAction::Move, + preferred: DndAction::Move, + }); + } + } + } + if !pushed { let bounds = layout.bounds(); log::trace!( @@ -2165,15 +2195,15 @@ where } struct TabDragSource { - payload: Box Option<(String, Vec)>>, + mime: String, threshold: f32, _marker: PhantomData, } impl TabDragSource { - fn new(payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static) -> Self { + fn new(mime: String) -> Self { Self { - payload: Box::new(payload), + mime, threshold: 8.0, _marker: PhantomData, } @@ -2254,6 +2284,8 @@ pub struct LocalState { wheel_timestamp: Option, /// Dnd state pub dnd_state: crate::widget::dnd_destination::State>, + /// Dnd state + pub offer_mimes: Vec, /// Tracks multi-touch events fingers_pressed: HashSet, /// The currently pressed item @@ -2391,6 +2423,7 @@ mod tests { tab_drag_candidate: None, dragging_tab: Some(dragging), drop_hint: None, + offer_mimes: Vec::new(), }; state.buttons_visible = len; state.known_length = len; From beddbf17703728182395a13267954d839226331d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 22 Jan 2026 10:21:05 -0500 Subject: [PATCH 019/168] improv(segmented_button): dnd state handling --- src/widget/segmented_button/widget.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 7a01749e..4206e727 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -968,17 +968,20 @@ where } else { mime_types.clone() }; - state.offer_mimes = mimes.clone(); + state.offer_mimes.clone_from(&mimes); _ = state .dnd_state .on_enter::(*x, *y, mimes, on_dnd_enter, entity); } DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} - DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) - if Some(my_id) == *id => + DndEvent::Offer(id, leave) + if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination) + && Some(my_id) == *id => { - state.dragging_tab = None; + if matches!(leave, OfferEvent::Leave) { + state.dragging_tab = None; + } state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { From 927035809f1564674434c27cbecdc67e199db28e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 22 Jan 2026 15:49:47 -0500 Subject: [PATCH 020/168] refactor(segmented button): only clear tab drag after source event cancel or finish --- src/widget/segmented_button/widget.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 4206e727..9d276be8 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1121,7 +1121,6 @@ where shell.publish(msg); } state.drop_hint = None; - state.dragging_tab = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { @@ -1136,7 +1135,6 @@ where "data received without entity id={my_id:?}" ); state.drop_hint = None; - state.dragging_tab = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { From f1c43f79abd4d5c0c610241def1d51f5ba0fbe3a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 24 Jan 2026 17:02:07 +0100 Subject: [PATCH 021/168] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aman Alam Co-authored-by: Baurzhan Muftakhidinov Co-authored-by: Hosted Weblate Co-authored-by: Jun Hwi Ku Co-authored-by: Walter William Beckerleg Bruckman Co-authored-by: gift983 <983649@my.leicestercollege.ac.uk> Co-authored-by: summoner001 Co-authored-by: 김유빈 Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hu/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kk/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/hu/libcosmic.ftl | 2 +- i18n/kk/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/ko/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/pa/libcosmic.ftl | 0 i18n/ti/libcosmic.ftl | 0 i18n/zh-Hans/libcosmic.ftl | 1 + 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 i18n/pa/libcosmic.ftl create mode 100644 i18n/ti/libcosmic.ftl diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl index 583fbe5c..02069244 100644 --- a/i18n/hu/libcosmic.ftl +++ b/i18n/hu/libcosmic.ftl @@ -2,7 +2,7 @@ close = Bezárás # About license = Licenc -links = Linkek +links = Hivatkozások developers = Fejlesztők designers = Tervezők artists = Művészek diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl index e69de29b..bb06e98f 100644 --- a/i18n/kk/libcosmic.ftl +++ b/i18n/kk/libcosmic.ftl @@ -0,0 +1,27 @@ +close = Жабу +license = Лицензия +links = Сілтемелер +developers = Әзірлеушілер +designers = Дизайнерлер +artists = Суретшілер +translators = Аудармашылар +documenters = Құжаттаушылар +january = Қаңтар { $year } +february = Ақпан { $year } +march = Наурыз { $year } +april = Сәуір { $year } +may = Мамыр { $year } +june = Маусым { $year } +july = Шілде { $year } +august = Тамыз { $year } +september = Қыркүйек { $year } +october = Қазан { $year } +november = Қараша { $year } +december = Желтоқсан { $year } +monday = Дс +tuesday = Сс +wednesday = Ср +thursday = Бс +friday = Жм +saturday = Сб +sunday = Жс diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index e69de29b..8d499756 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -0,0 +1,27 @@ +february = { $year }년 2월 +close = 닫기 +documenters = 문서 작성자 +november = { $year }년 11월 +friday = 금 +tuesday = 화 +may = { $year }년 5월 +wednesday = 수 +april = { $year }년 4월 +monday = 월 +translators = 번역가 +artists = 아티스트 +license = 라이선스 +december = { $year }년 12월 +sunday = 일 +links = 링크 +march = { $year }년 3월 +june = { $year }년 6월 +saturday = 토 +august = { $year }년 8월 +developers = 개발자 +july = { $year }년 7월 +thursday = 목 +september = { $year }년 9월 +designers = 디자이너 +october = { $year }년 10월 +january = { $year }년 1월 diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ti/libcosmic.ftl b/i18n/ti/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index 5d9fbd66..9dfd6139 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -24,3 +24,4 @@ friday = 周五 saturday = 周六 sunday = 周日 artists = 艺术家 +documenters = 文档作者 From 9fcd449611d30891d5fe5272520672da5ef6a723 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 27 Jan 2026 13:38:58 -0500 Subject: [PATCH 022/168] fix(segmented_button): hover state handling when hover state changes, paragraphs also need to be updated. I'll make a not to check this again after the rebase though. --- src/widget/segmented_button/widget.rs | 182 +++++++++++++++----------- 1 file changed, 106 insertions(+), 76 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 9d276be8..4f68b3de 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -242,6 +242,49 @@ where } } + fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { + if let Some(text) = self.model.text.get(key) { + let font = if self.button_is_focused(state, key) { + self.font_active + } else if state.show_context.is_some() || self.button_is_hovered(state, key) { + self.font_hovered + } else if self.model.is_active(key) { + self.font_active + } else { + self.font_inactive + }; + + let mut hasher = DefaultHasher::new(); + text.hash(&mut hasher); + font.hash(&mut hasher); + let text_hash = hasher.finish(); + + if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { + if prev_hash == text_hash { + return; + } + } + + let text = Text { + content: text.as_ref(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + }; + + if let Some(paragraph) = state.paragraphs.get_mut(key) { + paragraph.update(text); + } else { + state.paragraphs.insert(key, crate::Plain::new(text)); + } + } + } + pub fn context_menu(mut self, context_menu: Option>>) -> Self where Message: Clone + 'static, @@ -761,6 +804,14 @@ where SelectionMode: Default, Message: 'static + Clone, { + fn id(&self) -> Option { + Some(self.id.0.clone()) + } + + fn set_id(&mut self, id: widget::Id) { + self.id = Id(id); + } + fn children(&self) -> Vec { let mut children = Vec::new(); @@ -812,46 +863,7 @@ where let state = tree.state.downcast_mut::(); for key in self.model.order.iter().copied() { - if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) { - self.font_active - } else if state.show_context.is_some() || self.button_is_hovered(state, key) { - self.font_hovered - } else if self.model.is_active(key) { - self.font_active - } else { - self.font_inactive - }; - - let mut hasher = DefaultHasher::new(); - text.hash(&mut hasher); - font.hash(&mut hasher); - let text_hash = hasher.finish(); - - if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { - if prev_hash == text_hash { - continue; - } - } - - let text = Text { - content: text.as_ref(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::None, - line_height: self.line_height, - }; - - if let Some(paragraph) = state.paragraphs.get_mut(key) { - paragraph.update(text); - } else { - state.paragraphs.insert(key, crate::Plain::new(text)); - } - } + self.update_entity_paragraph(state, key); } // Diff the context menu @@ -899,9 +911,8 @@ where shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, ) -> event::Status { - let bounds = layout.bounds(); + let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); - state.hovered = Item::None; let my_id = self.get_drag_id(); @@ -938,7 +949,7 @@ where }, ) if Some(my_id) == *id => { let entity = self - .variant_bounds(state, bounds) + .variant_bounds(state, my_bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, @@ -947,7 +958,7 @@ where .map(|(key, _)| key); state.drop_hint = self.drop_hint_for_position( state, - bounds, + my_bounds, Point::new(*x as f32, *y as f32), ); self.emit_drop_hint(shell, state.drop_hint); @@ -979,9 +990,6 @@ where if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination) && Some(my_id) == *id => { - if matches!(leave, OfferEvent::Leave) { - state.dragging_tab = None; - } state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { @@ -1001,7 +1009,7 @@ where "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}" ); let new = self - .variant_bounds(state, bounds) + .variant_bounds(state, my_bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, @@ -1018,11 +1026,15 @@ where ); state.drop_hint = self.drop_hint_for_position( state, - bounds, + my_bounds, Point::new(*x as f32, *y as f32), ); self.emit_drop_hint(shell, state.drop_hint); if Some(Some(new_entity)) != entity { + state.hovered = Item::Tab(new_entity); + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } let prev_action = state .dnd_state .drag_offer @@ -1039,6 +1051,10 @@ where } } } else if entity.is_some() { + state.hovered = Item::None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } log::trace!( target: TAB_REORDER_LOG_TARGET, "offer motion leaving id={my_id:?}" @@ -1124,31 +1140,24 @@ where self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { + state.focused_item = Item::Tab(event.dragged); + state.hovered = Item::None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } if let Some(on_reorder) = self.on_reorder.as_ref() { shell.publish(on_reorder(event)); + return event::Status::Captured; } } return ret; - } else { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "data received without entity id={my_id:?}" - ); - state.drop_hint = None; - - self.emit_drop_hint(shell, state.drop_hint); - if let Some(event) = pending_reorder { - if let Some(on_reorder) = self.on_reorder.as_ref() { - shell.publish(on_reorder(event)); - } - } } } _ => {} } } - if cursor_position.is_over(bounds) { + if cursor_position.is_over(my_bounds) { let fingers_pressed = state.fingers_pressed.len(); match event { @@ -1166,10 +1175,14 @@ where // Check for clicks on the previous and next tab buttons, when tabs are collapsed. if state.collapsed { // Check if the prev tab button was clicked. - if cursor_position.is_over(prev_tab_bounds(&bounds, f32::from(self.button_height))) + if cursor_position + .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height))) && self.prev_tab_sensitive(state) { state.hovered = Item::PrevButton; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { @@ -1178,11 +1191,13 @@ where } else { // Check if the next tab button was clicked. if cursor_position - .is_over(next_tab_bounds(&bounds, f32::from(self.button_height))) + .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height))) && self.next_tab_sensitive(state) { state.hovered = Item::NextButton; - + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { @@ -1193,7 +1208,7 @@ where } for (key, bounds) in self - .variant_bounds(state, bounds) + .variant_bounds(state, my_bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, @@ -1203,7 +1218,12 @@ where if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. - state.hovered = Item::Tab(key); + if state.hovered != Item::Tab(key) { + state.hovered = Item::Tab(key); + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } + } let close_button_bounds = close_bounds(bounds, f32::from(self.close_icon.size)); @@ -1320,6 +1340,9 @@ where } break; + } else if state.hovered == Item::Tab(key) { + state.hovered = Item::None; + self.update_entity_paragraph(state, key); } } @@ -1377,15 +1400,22 @@ where } } } - } else if state.is_focused() { - // Unfocus on clicks outside of the boundaries of the segmented button. - if is_pressed(&event) { - state.unfocus(); - state.pressed_item = None; - return event::Status::Ignored; + } else { + if let Item::Tab(key) = std::mem::replace(&mut state.hovered, Item::None) { + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } + } + if state.is_focused() { + // Unfocus on clicks outside of the boundaries of the segmented button. + if is_pressed(&event) { + state.unfocus(); + state.pressed_item = None; + return event::Status::Ignored; + } + } else if is_lifted(&event) { + state.pressed_item = None; } - } else if is_lifted(&event) { - state.pressed_item = None; } if let (Some(tab_drag), Some(candidate)) = From b71a7c9edffa6b278836da90106976ded9e90159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:38:23 +0100 Subject: [PATCH 023/168] improv: remove double coloring of `content_container` windows This sets the main content and the header bar to transparent when `content_container` is true, so that things aren't colored twice and overlayed on top of each other. This ensures that modifying color alpha behaves as expected, especially for frosted glass. --- src/app/mod.rs | 2 +- src/theme/style/iced.rs | 8 +++++++- src/widget/header_bar.rs | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 090698df..67636dac 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -689,7 +689,6 @@ impl ApplicationExt for App { .apply(container) .width(iced::Length::Fill) .height(iced::Length::Fill) - .class(crate::theme::Container::WindowBackground) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { @@ -713,6 +712,7 @@ impl ApplicationExt for App { .focused(focused) .maximized(maximized) .sharp_corners(sharp_corners) + .transparent(content_container) .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 32309860..937ee388 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -396,6 +396,7 @@ pub enum Container<'a> { HeaderBar { focused: bool, sharp_corners: bool, + transparent: bool, }, List, Primary, @@ -511,6 +512,7 @@ impl iced_container::Catalog for Theme { Container::HeaderBar { focused, sharp_corners, + transparent, } => { let (icon_color, text_color) = if *focused { ( @@ -527,7 +529,11 @@ impl iced_container::Catalog for Theme { iced_container::Style { icon_color: Some(icon_color), text_color: Some(text_color), - background: Some(iced::Background::Color(cosmic.background.base.into())), + background: if *transparent { + None + } else { + Some(iced::Background::Color(cosmic.background.base.into())) + }, border: Border { radius: [ if *sharp_corners { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index d500bde3..c5bde28f 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -28,6 +28,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { is_ssd: false, on_double_click: None, is_condensed: false, + transparent: false, } } @@ -92,6 +93,9 @@ pub struct HeaderBar<'a, Message> { /// Whether the headerbar should be compact is_condensed: bool, + + /// Whether the headerbar should be transparent + transparent: bool, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -412,6 +416,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, + transparent: self.transparent, }) .center_y(Length::Shrink) .apply(widget::mouse_area); From cf19ac665f353bbca0bad945403976ccdf6c8191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:49:48 +0100 Subject: [PATCH 024/168] chore: update dependencies --- Cargo.toml | 16 ++++++++-------- cosmic-config-derive/Cargo.toml | 4 ++-- cosmic-config-derive/src/lib.rs | 4 ++-- cosmic-config/Cargo.toml | 6 +++--- cosmic-theme/Cargo.toml | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46091bcc..feaa8c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "libcosmic" -version = "0.1.0" +version = "1.0.0" edition = "2024" -rust-version = "1.85" +rust-version = "1.90" [lib] name = "cosmic" @@ -104,7 +104,7 @@ async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.7" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } -chrono = "0.4.42" +chrono = "0.4.43" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } # Internationalization @@ -126,26 +126,26 @@ log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" raw-window-handle = "0.6" -rfd = { version = "0.15.4", default-features = false, features = [ +rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } rustix = { version = "1.1", features = ["pipe", "process"], optional = true } serde = { version = "1.0.228", features = ["derive"] } slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } -thiserror = "2.0.17" +thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } tokio = { version = "1.49.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.13.1", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus = { version = "5.13.1", default-features = false } +zbus = { version = "5.13.2", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -209,7 +209,7 @@ git = "https://github.com/pop-os/cosmic-panel" optional = true [dependencies.ron] -version = "0.11" +version = "0.12" optional = true [workspace] diff --git a/cosmic-config-derive/Cargo.toml b/cosmic-config-derive/Cargo.toml index 55eeb871..9d5f4b88 100644 --- a/cosmic-config-derive/Cargo.toml +++ b/cosmic-config-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cosmic-config-derive" -version = "0.1.0" -edition = "2021" +version = "1.0.0" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 668154cd..cc19a91e 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -106,7 +106,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { }) }); - let gen = quote! { + let generate = quote! { impl CosmicConfigEntry for #name { const VERSION: u64 = #version; @@ -147,5 +147,5 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { } }; - gen.into() + generate.into() } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 78d671ca..6103c15e 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-config" -version = "0.1.0" +version = "1.0.0" edition = "2024" [features] @@ -11,11 +11,11 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.13.1", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.3", optional = true } notify = "8.2.0" -ron = "0.11.0" +ron = "0.12.0" serde = "1.0.228" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } iced = { path = "../iced/", default-features = false, optional = true } diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 10b548b4..cf6afe74 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-theme" -version = "0.1.0" +version = "1.0.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -21,11 +21,11 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } -ron = "0.11.0" -csscolorparser = { version = "0.7.2", features = ["serde"] } +ron = "0.12.0" +csscolorparser = { version = "0.8.1", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", ] } dirs.workspace = true -thiserror = "2.0.17" +thiserror = "2.0.18" From fdcba7d8ececc35c09a7871b018930f752ac784b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 28 Jan 2026 18:04:55 -0500 Subject: [PATCH 025/168] fix(segmented_button): dnd hover --- src/widget/segmented_button/widget.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 4f68b3de..e4f416bf 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -966,6 +966,13 @@ where target: TAB_REORDER_LOG_TARGET, "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" ); + // force hovered state update + if let Some(entity) = entity { + state.hovered = Item::Tab(entity); + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } + } let on_dnd_enter = self .on_dnd_enter @@ -1001,6 +1008,10 @@ where target: TAB_REORDER_LOG_TARGET, "offer leave id={my_id:?} entity={entity:?}" ); + state.hovered = Item::None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } _ = state.dnd_state.on_leave::(None); } DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { From 3e78eb238159d90956e85e95e868164671b649f6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 30 Jan 2026 21:07:58 +0100 Subject: [PATCH 026/168] i18n: translation updates from weblate Co-authored-by: Hafidz Nasruddin Co-authored-by: Hosted Weblate Co-authored-by: Languages add-on Co-authored-by: Zahid Rizky Fakhri Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/id/ Translation: Pop OS/libcosmic --- i18n/id/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/ms/libcosmic.ftl | 0 i18n/uz/libcosmic.ftl | 0 3 files changed, 27 insertions(+) create mode 100644 i18n/ms/libcosmic.ftl create mode 100644 i18n/uz/libcosmic.ftl diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic.ftl index e69de29b..2ce82dab 100644 --- a/i18n/id/libcosmic.ftl +++ b/i18n/id/libcosmic.ftl @@ -0,0 +1,27 @@ +close = Tutup +license = Lisensi +links = Tautan +developers = Pengembang +designers = Perancang +artists = Artis +translators = Penerjemah +documenters = Dokumenter +january = Januari { $year } +february = Februari { $year } +march = Maret { $year } +april = April { $year } +may = Mei { $year } +june = Juni { $year } +july = Juli { $year } +august = Agustus { $year } +september = September { $year } +october = Oktober { $year } +november = November { $year } +december = Desember { $year } +monday = Sen +tuesday = Sel +wednesday = Rab +sunday = Min +saturday = Sab +friday = Jum +thursday = Kam diff --git a/i18n/ms/libcosmic.ftl b/i18n/ms/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/uz/libcosmic.ftl b/i18n/uz/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 30a02ec0bb3cccabb664572d98a77740ab56c2fe Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 7 Feb 2026 22:08:52 +0100 Subject: [PATCH 027/168] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aliaksandr Truš Co-authored-by: Drugi Sapog Co-authored-by: Hosted Weblate Co-authored-by: Quentin PAGÈS Co-authored-by: jickson john Co-authored-by: jonnysemon Co-authored-by: Димко Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/be/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 21 ++++++++++++++++++++- i18n/be/libcosmic.ftl | 19 +++++++++++++++++++ i18n/ml/libcosmic.ftl | 0 i18n/oc/libcosmic.ftl | 0 i18n/uk/libcosmic.ftl | 2 +- 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 i18n/ml/libcosmic.ftl create mode 100644 i18n/oc/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index 428bd892..ce3eb1e8 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -4,7 +4,26 @@ close = أغلِق license = الترخيص links = الروابط developers = المطورون -designers = المصممون +designers = المصمّمون artists = الفنانون translators = المترجمون documenters = الموثقون +january = يناير { $year } +february = فبراير { $year } +march = مارس { $year } +april = ابريل { $year } +may = مايو { $year } +june = يونيو { $year } +july = يوليو { $year } +august = أغسطس { $year } +september = سبتمبر { $year } +october = أكتوبر { $year } +november = نوفمبر { $year } +december = ديسمبر { $year } +monday = الاثنين +tuesday = الثلاثاء +wednesday = الأربعاء +thursday = الخميس +friday = الجمعة +saturday = السبت +sunday = الأحد diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic.ftl index eb3abf33..1682a174 100644 --- a/i18n/be/libcosmic.ftl +++ b/i18n/be/libcosmic.ftl @@ -6,3 +6,22 @@ designers = Дызайнеры artists = Мастакі translators = Перакладчыкі documenters = Дакументалісты +february = Люты { $year } +november = Лістапад { $year } +friday = Пт +tuesday = Аў +may = Май { $year } +wednesday = Ср +april = Красавік { $year } +monday = Пн +december = Снежань { $year } +sunday = Нд +march = Сакавік { $year } +june = Чэрвень { $year } +saturday = Сб +august = Жнівень { $year } +july = Ліпень { $year } +thursday = Чц +september = Верасень { $year } +october = Кастрычнік { $year } +january = Студзень { $year } diff --git a/i18n/ml/libcosmic.ftl b/i18n/ml/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index 73278ae4..d82c2a6e 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -2,7 +2,7 @@ close = Закрити # About license = Ліцензія -links = Посилання +links = Ланки developers = Розробники designers = Дизайнери artists = Художники From a3cf875793aa56bda4963bd5eaa8877a4d3aefb0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 9 Feb 2026 22:04:13 +0100 Subject: [PATCH 028/168] fix(single-instance): unminimize main window on dbus activate --- src/app/cosmic.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index ae554846..803a56bd 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -369,7 +369,16 @@ where crate::Action::Cosmic(message) => self.cosmic_update(message), crate::Action::None => iced::Task::none(), #[cfg(feature = "single-instance")] - crate::Action::DbusActivation(message) => self.app.dbus_activation(message), + crate::Action::DbusActivation(message) => { + let mut task = self.app.dbus_activation(message); + + if let Some(id) = self.app.core().main_window_id() { + let unminimize = iced_runtime::window::minimize::<()>(id, false); + task = task.chain(unminimize.discard()); + } + + task + } }; #[cfg(all(target_env = "gnu", not(target_os = "windows")))] From ae830ca21dd9f2da3c1f4a1617daeec126d3867e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 12 Feb 2026 15:52:40 +0100 Subject: [PATCH 029/168] perf(font): use RwLock when getting fonts instead of Mutex --- src/config/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 1253ce8d..5a96a5e1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,7 +8,7 @@ use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_config::{Config, CosmicConfigEntry}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; -use std::sync::{LazyLock, Mutex, RwLock}; +use std::sync::{LazyLock, RwLock}; /// ID for the `CosmicTk` config. pub const ID: &str = "com.system76.CosmicTk"; @@ -17,7 +17,7 @@ const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono"; const SANS_FAMILY_DEFAULT: &str = "Open Sans"; /// Stores static strings of the family names for `iced::Font` compatibility. -pub static FAMILY_MAP: LazyLock>> = LazyLock::new(Mutex::default); +pub static FAMILY_MAP: LazyLock>> = LazyLock::new(RwLock::default); pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { RwLock::new( @@ -156,14 +156,14 @@ pub struct FontConfig { impl From for iced::Font { fn from(font: FontConfig) -> Self { - let mut family_map = FAMILY_MAP.lock().unwrap(); - - let name: &'static str = family_map + let name = FAMILY_MAP + .read() + .unwrap() .get(font.family.as_str()) .copied() .unwrap_or_else(|| { - let value = font.family.clone().leak(); - family_map.insert(value); + let value: &'static str = font.family.clone().leak(); + FAMILY_MAP.write().unwrap().insert(value); value }); From 031818c6b08d706459f41a793e99338dd922bdbe Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 13 Feb 2026 18:30:14 +0100 Subject: [PATCH 030/168] fix(font): explicitly drop read guard in on font family lookup --- src/config/mod.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 5a96a5e1..9807961c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -16,9 +16,6 @@ pub const ID: &str = "com.system76.CosmicTk"; const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono"; const SANS_FAMILY_DEFAULT: &str = "Open Sans"; -/// Stores static strings of the family names for `iced::Font` compatibility. -pub static FAMILY_MAP: LazyLock>> = LazyLock::new(RwLock::default); - pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { RwLock::new( CosmicTk::config() @@ -156,16 +153,19 @@ pub struct FontConfig { impl From for iced::Font { fn from(font: FontConfig) -> Self { - let name = FAMILY_MAP - .read() - .unwrap() - .get(font.family.as_str()) - .copied() - .unwrap_or_else(|| { - let value: &'static str = font.family.clone().leak(); - FAMILY_MAP.write().unwrap().insert(value); - value - }); + /// Stores static strings of the family names for `iced::Font` compatibility. + static FAMILY_MAP: LazyLock>> = + LazyLock::new(RwLock::default); + + let read_guard = FAMILY_MAP.read().unwrap(); + let name: Option<&'static str> = read_guard.get(font.family.as_str()).copied(); + drop(read_guard); + + let name = name.unwrap_or_else(|| { + let value: &'static str = font.family.clone().leak(); + FAMILY_MAP.write().unwrap().insert(value); + value + }); Self { family: iced::font::Family::Name(name), From ae1f15f37ee6d1fde579c6a6557e44b1a208f95e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 13 Feb 2026 12:36:03 -0700 Subject: [PATCH 031/168] Add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..e6ca28bc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +- [ ] I have disclosed use of any AI generated code in my commit messages. + - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. + - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. +- [ ] I understand these changes in full and will be able to respond to review comments. +- [ ] My change is accurately described in the commit message. +- [ ] My contribution is tested and working as described. +- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. + From 21c5a4f34a33795d7836ff673a360ef1472f7567 Mon Sep 17 00:00:00 2001 From: Frieder Hannenheim Date: Mon, 16 Feb 2026 15:41:35 +0000 Subject: [PATCH 032/168] feat(dnd_destination): xdg file transfer portal support Requires the `xdg-portal` feature to be enabled to use these features. - Adds `DndDestination::on_file_transfer` method to handle `application/vnd.portal.filetransfer` drop requests - Adds `command::file_transfer_receive` function to handle the file transfer request messages - Adds `command::file_transfer_send` to initiate a file transfer from the application --- src/command.rs | 24 ++++++++++++++++++++++++ src/widget/dnd_destination.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/command.rs b/src/command.rs index 73c900c1..14d326b4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,9 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +#[cfg(feature = "xdg-portal")] +use std::os::fd::AsFd; + use iced::window; /// Initiates a window drag. @@ -43,3 +46,24 @@ pub fn set_windowed(id: window::Id) -> iced::Task> { pub fn toggle_maximize(id: window::Id) -> iced::Task> { iced_runtime::window::toggle_maximize(id) } + +#[cfg(feature = "xdg-portal")] +pub fn file_transfer_send(writeable: bool, auto_stop: bool, files: Vec) -> iced::Task> { + iced::Task::future(async move { + let file_transfer = ashpd::documents::FileTransfer::new().await?; + let key = file_transfer.start_transfer(writeable, auto_stop).await?; + file_transfer.add_files(&key, &files).await?; + Ok(key) + }) +} + +/// Receive the files offered over the xdg share portal using the `key`. +/// Returns a list of file paths. +#[cfg(feature = "xdg-portal")] +pub fn file_transfer_receive(key: String) -> iced::Task>> { + dbg!(&key); + iced::Task::future(async move { + let file_transfer = ashpd::documents::FileTransfer::new().await?; + file_transfer.retrieve_files(&key).await + }) +} diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 947d2fe3..a32a9fba 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -40,6 +40,8 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; +#[cfg(feature = "xdg-portal")] +pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DragId(pub u128); @@ -73,6 +75,8 @@ pub struct DndDestination<'a, Message> { on_action_selected: Option Message>>, on_data_received: Option) -> Message>>, on_finish: Option, DndAction, f64, f64) -> Message>>, + #[cfg(feature = "xdg-portal")] + on_file_transfer: Option Message>>, } impl<'a, Message: 'static> DndDestination<'a, Message> { @@ -99,6 +103,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -124,6 +130,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_finish: Some(Box::new(move |mime, data, action, _, _| { on_finish(T::try_from((data, mime)).ok(), action) })), + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -159,6 +167,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -237,6 +247,20 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { self } + /// Add a message that will be emitted instead of [`on_data_received`](Self::on_data_received) if the dropped files + /// are offered through the xdg share portal. You can then use [`crate::command::file_transfer_receive`] + /// with the key to receive the files. + #[cfg(feature = "xdg-portal")] + #[must_use] + pub fn on_file_transfer(mut self, f: impl Fn(String) -> Message + 'static) -> Self { + match self.mime_types.iter().position(|v| v == "text/uri-list") { + Some(i) => self.mime_types.insert(i, Cow::Borrowed(FILE_TRANSFER_MIME)), + None => self.mime_types.push(Cow::Borrowed(FILE_TRANSFER_MIME)), + } + self.on_file_transfer = Some(Box::new(f)); + self + } + /// Returns the drag id of the destination. /// /// # Panics @@ -496,6 +520,13 @@ impl Widget "offer data id={my_id:?} mime={mime_type:?} bytes={}", data.len() ); + + #[cfg(feature = "xdg-portal")] + if mime_type == FILE_TRANSFER_MIME && let Some(f) = self.on_file_transfer.as_ref() && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { + shell.publish(f(s)); + return event::Status::Captured; + } + if let (Some(msg), ret) = state.on_data_received( mime_type, data, From 6328c40ef763e165f365d9af680912348414d17b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 16 Feb 2026 16:51:02 +0100 Subject: [PATCH 033/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2db5545f..e2a24417 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2db5545fbee505c2c643c628a8984d1666c4d451 +Subproject commit e2a2441789a7e302f099c0e8e9493ef81b58e265 From a2e903ad94c6c2728c22454f098a81cb10f212bc Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Tue, 17 Feb 2026 16:39:37 +0000 Subject: [PATCH 034/168] feat(cosmic-theme): add color schemes for qt apps --- cosmic-theme/Cargo.toml | 1 + cosmic-theme/src/model/theme.rs | 3 + cosmic-theme/src/output/gtk4_output.rs | 16 +- cosmic-theme/src/output/mod.rs | 30 ++ cosmic-theme/src/output/qt56ct_output.rs | 113 +++++ cosmic-theme/src/output/qt_output.rs | 517 +++++++++++++++++++++++ 6 files changed, 677 insertions(+), 3 deletions(-) create mode 100644 cosmic-theme/src/output/qt56ct_output.rs create mode 100644 cosmic-theme/src/output/qt_output.rs diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index cf6afe74..80f4805d 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -27,5 +27,6 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features "subscription", "macro", ] } +configparser = "3.1.0" dirs.workspace = true thiserror = "2.0.18" diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 1f94f5a2..cef479ae 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -690,6 +690,9 @@ impl Theme { let config = Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?; let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; + Self::get_active_with_brightness(is_dark) + } + pub fn get_active_with_brightness(is_dark: bool) -> Result, Self)> { let config = if is_dark { Self::dark_config() } else { diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 6fdf26d5..40eba5b4 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -163,9 +163,19 @@ impl Theme { std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; } - let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?; - file.write_all(css_str.as_bytes()) - .map_err(OutputError::Io)?; + let file_path = config_dir.join(name); + let tmp_file_path = config_dir.join(name.to_owned() + "~"); + + // Write to tmp_file_path first, then move it to file_path + let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; + let res = tmp_file + .write_all(css_str.as_bytes()) + .and_then(|_| tmp_file.flush()) + .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); + if let Err(e) = res { + _ = std::fs::remove_file(&tmp_file_path); + return Err(OutputError::Io(e)); + } Ok(()) } diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 832771d4..61f0e49d 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -6,6 +6,11 @@ use crate::Theme; /// Module for outputting the Cosmic gtk4 theme type as CSS pub mod gtk4_output; +/// Module for configuring qt5ct and qt6ct to use our qt theme +pub mod qt56ct_output; +/// Module for outputting the Cosmic qt theme type as kdeglobals +pub mod qt_output; + pub mod vs_code; #[derive(Error, Debug)] @@ -14,32 +19,57 @@ pub enum OutputError { Io(std::io::Error), #[error("Missing config directory")] MissingConfigDir, + #[error("Missing data directory")] + MissingDataDir, #[error("Serde Error: {0}")] Serde(#[from] serde_json::Error), + #[error("Ini Error: {0}")] + Ini(String), } impl Theme { #[inline] pub fn apply_exports(&self) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(self.is_dark); + let qt_res = Theme::apply_qt(self.is_dark); + let qt56ct_res = Theme::apply_qt56ct(self.is_dark); let vs_res = self.clone().apply_vs_code(); gtk_res?; + qt_res?; + qt56ct_res?; vs_res?; Ok(()) } + #[inline] + /// To avoid rewriting too much code, I replaced calls to `Theme::apply_gtk` with this. + /// Note that vscode isn't touched by this function. + pub fn apply_exports_static(is_dark: bool) -> Result<(), OutputError> { + let gtk_res = Theme::apply_gtk(is_dark); + let qt_res = Theme::apply_qt(is_dark); + let qt56ct_res = Theme::apply_qt56ct(is_dark); + gtk_res?; + qt_res?; + qt56ct_res?; + Ok(()) + } + #[inline] pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); + let qt_res = self.write_qt(); gtk_res?; + qt_res?; Ok(()) } #[inline] pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); + let qt_res = Theme::reset_qt(); let vs_res = Theme::reset_vs_code(); gtk_res?; + qt_res?; vs_res?; Ok(()) } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs new file mode 100644 index 00000000..d4736597 --- /dev/null +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -0,0 +1,113 @@ +use crate::Theme; +use configparser::ini::Ini; +use std::{ + fs::{self, File}, + path::PathBuf, +}; + +use super::OutputError; + +impl Theme { + /// The "version" of this theme. + /// + /// To avoid repeatedly overwriting the user's config, we use a version system. + /// + /// Increment this value when changes to qt{5,6}ct.conf are needed. + /// If the config's version is outdated, we update several sections. + /// Otherwise, only the light/dark mode is updated. + const COSMIC_QT_VERSION: u64 = 1; + + /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. + #[cold] + pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> { + let qt5ct_res = Self::apply_ct("qt5ct", is_dark); + let qt6ct_res = Self::apply_ct("qt6ct", is_dark); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn apply_ct(ct: &str, is_dark: bool) -> Result<(), OutputError> { + let path = Self::get_conf_path(ct)?; + let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; + let mut ini = Ini::new_cs(); + ini.read(file_content).map_err(OutputError::Ini)?; + + let old_version = ini + .getuint("Appearance", "cosmic_qt_version") + .map_err(OutputError::Ini)? + .unwrap_or_default(); + + let color_scheme_path = Self::get_qt_colors_path(is_dark)?; + let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; + + ini.set( + "Appearance", + "cosmic_qt_version", + Some(Theme::COSMIC_QT_VERSION.to_string()), + ); + + if old_version < Theme::COSMIC_QT_VERSION { + // Config is outdated, update it unconditionally! + + ini.setstr( + "Appearance", + "color_scheme_path", + color_scheme_path.to_str(), + ); + // Enable the above color scheme, instead of using the default color scheme of e.g. Breeze + ini.setstr("Appearance", "custom_palette", Some("true")); + // COSMIC icons are stuck in light mode, so use breeze icons instead + ini.setstr("Appearance", "icon_theme", Some(icon_theme)); + // Use COSMIC dialogs instead of KDE's + ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal")); + + // TODO: Add fonts section to match COSMIC + } else { + // Config is not outdated, check before updating light/dark mode only! + + let old_color_scheme_path = ini + .get("Appearance", "color_scheme_path") + .unwrap_or_else(|| "CosmicPlease".to_owned()); + if old_color_scheme_path.contains("Cosmic") { + ini.setstr( + "Appearance", + "color_scheme_path", + color_scheme_path.to_str(), + ); + } + + let old_icon_theme = ini + .get("Appearance", "icon_theme") + .unwrap_or_else(|| "breeze".to_owned()); + if old_icon_theme.contains("breeze") { + ini.setstr("Appearance", "icon_theme", Some(icon_theme)); + } + } + + ini.write(path).map_err(OutputError::Io)?; + Ok(()) + } + + /// Returns the file paths of the form `~/.config/ct/ct.conf`: + /// e.g. `~/.config/qt6ct/qt6ct.conf`. + /// + /// The file and its parent directory are created if they don't exist. + fn get_conf_path(ct: &str) -> Result { + let Some(mut config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + config_dir.push(&ct); + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; + } + + let file_path = config_dir.join(ct.to_owned() + ".conf"); + if !file_path.exists() { + File::create_new(&file_path).map_err(OutputError::Io)?; + } + + Ok(file_path) + } +} diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs new file mode 100644 index 00000000..0d9a4258 --- /dev/null +++ b/cosmic-theme/src/output/qt_output.rs @@ -0,0 +1,517 @@ +use crate::Theme; +use configparser::ini::Ini; +use palette::{Mix, Srgba, blend::Compose}; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use super::OutputError; + +impl Theme { + /// Produces a color scheme ini file for Qt. + /// + /// Some high-level documentation for this file can be found at: + /// https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ + #[must_use] + #[cold] + pub fn as_qt(&self) -> String { + // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter + let disabled_color_effects = IniColorEffects { + color: self.button.disabled, + color_amount: 0.0, + color_effect: ColorEffect::Desaturate, + contrast_amount: 0.65, + contrast_effect: ColorEffect::Fade, + intensity_amount: 0.1, + intensity_effect: IntensityEffect::Lighten, + }; + // Usually, inactive elements will have reduced contrast (text fades slightly into the background) and may have slightly reduced intensity + let inactive_color_effects = IniColorEffects { + color: self.palette.gray_1, + color_amount: 0.025, + color_effect: ColorEffect::Tint, + contrast_amount: 0.1, + contrast_effect: ColorEffect::Tint, + intensity_amount: 0.0, + intensity_effect: IntensityEffect::Shade, + }; + + let bg = self.background.base; + // the background container + let view_colors = IniColors { + background_alternate: bg.mix(self.accent.base, 0.05), + background_normal: bg, + decoration_focus: self.accent_text_color(), + decoration_hover: self.accent_text_color(), + foreground_active: self.accent_text_color(), + foreground_inactive: self.background.on.mix(bg, 0.1), + foreground_link: self.link_button.base, + foreground_negative: self.destructive_text_color(), + foreground_neutral: self.warning_text_color(), + foreground_normal: self.background.on, + foreground_positive: self.success_text_color(), + foreground_visited: self.accent_text_color(), + }; + // components inside the background container + let window_colors = IniColors { + background_alternate: self.background.component.base.mix(self.accent.base, 0.05), + background_normal: self.background.component.base, + ..view_colors + }; + + // selected text and items + let selection_colors = { + let selected = self.background.component.selected; + let selected_text = self.background.component.selected_text; + IniColors { + background_alternate: selected.mix(bg, 0.5), + background_normal: selected, + decoration_focus: selected, + decoration_hover: selected, + foreground_active: selected_text, + foreground_inactive: selected_text.mix(selected, 0.5), + foreground_link: self.link_button.on, + foreground_negative: self.destructive_color(), + foreground_neutral: self.warning_color(), + foreground_normal: selected_text, + foreground_positive: self.success_color(), + foreground_visited: self.accent_color(), + } + }; + + let button_colors = IniColors { + background_alternate: self.accent_button.base, + background_normal: self.button.base, + ..view_colors + }; + + // Complementary: Areas of applications with an alternative color scheme; usually with a dark background for light color schemes. + let complementary_colors = { + let dark = if self.is_dark { + self.clone() + } else { + Self::get_active_with_brightness(false).unwrap_or_else(|_| self.clone()) + }; + IniColors { + background_alternate: dark.accent.base, + background_normal: dark.background.base, + decoration_focus: dark.accent_text_color(), + decoration_hover: dark.accent_text_color(), + foreground_active: dark.accent_text_color(), + foreground_inactive: dark.background.on.mix(dark.background.base, 0.1), + foreground_link: dark.link_button.base, + foreground_negative: dark.destructive_text_color(), + foreground_neutral: dark.warning_text_color(), + foreground_normal: dark.background.on, + foreground_positive: dark.success_text_color(), + foreground_visited: dark.accent_text_color(), + } + }; + + // headers in cosmic don't have a background + let header_colors = &view_colors; + let header_colors_inactive = &view_colors; + // tool tips, "What's This" tips, and similar elements + let tooltip_colors = &window_colors; + + let general_color_scheme = if self.is_dark { + "CosmicDark" + } else { + "CosmicLight" + }; + let general_name = if self.is_dark { + "COSMIC Dark" + } else { + "COSMIC Light" + }; + // COSMIC icons are stuck in light mode, so use breeze icons instead + let icons_theme = if self.is_dark { + "breeze-dark" + } else { + "breeze" + }; + + format!( + r#"# GENERATED BY COSMIC + +[ColorEffects:Disabled] +{} + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +{} + +[Colors:Button] +{} + +[Colors:Complementary] +{} + +[Colors:Header] +{} + +[Colors:Header][Inactive] +{} + +[Colors:Selection] +{} + +[Colors:Tooltip] +{} + +[Colors:View] +{} + +[Colors:Window] +{} + +[General] +ColorScheme={general_color_scheme} +Name={general_name} +shadeSortColumn=true + +[Icons] +Theme={icons_theme} + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +{} +"#, + format_ini_color_effects(&disabled_color_effects, bg), + format_ini_color_effects(&inactive_color_effects, bg), + format_ini_colors(&button_colors, bg), + format_ini_colors(&complementary_colors, bg), + format_ini_colors(&header_colors, bg), + format_ini_colors(&header_colors_inactive, bg), + format_ini_colors(&selection_colors, bg), + format_ini_colors(&tooltip_colors, bg), + format_ini_colors(&view_colors, bg), + format_ini_colors(&window_colors, bg), + format_ini_wm_colors(&view_colors, self.is_dark), + ) + } + + /// Write the color scheme to the appropriate directory. + /// Should be written in `~/.local/share/color-schemes/`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error writing the colors file. + #[cold] + pub fn write_qt(&self) -> Result<(), OutputError> { + let colors = self.as_qt(); + let file_path = Self::get_qt_colors_path(self.is_dark)?; + let tmp_file_path = file_path.with_extension("colors.new"); + + // Write to tmp_file_path first, then move it to file_path + let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; + let res = tmp_file + .write_all(colors.as_bytes()) + .and_then(|_| tmp_file.flush()) + .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); + if let Err(e) = res { + _ = std::fs::remove_file(&tmp_file_path); + return Err(OutputError::Io(e)); + } + + Ok(()) + } + + /// Apply the color scheme by copying its values to `~/.config/kdeglobals`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error applying the color scheme. + #[cold] + pub fn apply_qt(is_dark: bool) -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + let kdeglobals_file = config_dir.join("kdeglobals"); + let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; + + let src_file = Self::get_qt_colors_path(is_dark)?; + let src_ini = Self::read_ini(&src_file)?; + + Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) + .map_err(OutputError::Io)?; + + for (section, key_value) in src_ini.get_map_ref() { + for (key, value) in key_value { + kdeglobals_ini.set(section, key, value.clone()); + } + } + + kdeglobals_ini + .write(kdeglobals_file) + .map_err(OutputError::Io)?; + Ok(()) + } + + /// Reset the applied qt colors by removing color scheme values from the + /// `~/.config/kdeglobals` file. + /// + /// This does not restore the backed up kdeglobals file. + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error resetting the CSS file. + #[cold] + pub fn reset_qt() -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + let kdeglobals_file = config_dir.join("kdeglobals"); + let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; + + if !Self::is_cosmic_kdeglobals(&kdeglobals_ini) + .map_err(OutputError::Io)? + .unwrap_or_default() + { + // Not a cosmic kdeglobals file, do nothing + return Ok(()); + } + + let is_dark = false; // doesn't matter since we're only reading keys + let src_file = Self::get_qt_colors_path(is_dark)?; + let src_ini = Self::read_ini(&src_file)?; + + for (section, key_value) in src_ini.get_map_ref() { + for (key, _) in key_value { + kdeglobals_ini.remove_key(section, key); + } + } + + kdeglobals_ini + .write(kdeglobals_file) + .map_err(OutputError::Io)?; + Ok(()) + } + + /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` + pub fn get_qt_colors_path(is_dark: bool) -> Result { + let Some(mut data_dir) = dirs::data_dir() else { + return Err(OutputError::MissingDataDir); + }; + + let file_name = if is_dark { + "CosmicDark.colors" + } else { + "CosmicLight.colors" + }; + + data_dir.push("color-schemes"); + if !data_dir.exists() { + std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?; + } + + Ok(data_dir.join(file_name)) + } + + #[cold] + fn read_ini(path: &PathBuf) -> Result { + let mut ini = Ini::new_cs(); + if !path.exists() { + return Ok(ini); + } + let file_content = fs::read_to_string(path).map_err(OutputError::Io)?; + ini.read(file_content).map_err(OutputError::Ini)?; + Ok(ini) + } + + #[cold] + fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> { + if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) { + let backup_path = path.with_extension("bak"); + fs::rename(path, &backup_path)?; + } + Ok(()) + } + + #[cold] + fn is_cosmic_kdeglobals(ini: &Ini) -> io::Result> { + let color_scheme = ini.get("General", "ColorScheme"); + if let Some(color_scheme) = color_scheme { + Ok(Some( + color_scheme == "CosmicDark" || color_scheme == "CosmicLight", + )) + } else { + Ok(None) + } + } +} + +/// Formats a color in the form `r,g,b` e.g. `255,255,255`. +/// If the color has transparency, it is mixed with bg first. +fn to_rgb(c: Srgba, bg: Srgba) -> String { + let c_u8: Srgba = c.over(bg).into_format(); + format!("{},{},{}", c_u8.red, c_u8.green, c_u8.blue) +} + +fn format_ini_color_effects(color_effects: &IniColorEffects, bg: Srgba) -> String { + format!( + r#"Color={} +ColorAmount={} +ColorEffect={} +ContrastAmount={} +ContrastEffect={} +IntensityAmount={} +IntensityEffect={}"#, + to_rgb(color_effects.color, bg), + color_effects.color_amount, + color_effects.color_effect.as_u8(), + color_effects.contrast_amount, + color_effects.contrast_effect.as_u8(), + color_effects.intensity_amount, + color_effects.intensity_effect.as_u8(), + ) +} + +fn format_ini_colors(colors: &IniColors, bg: Srgba) -> String { + format!( + r#"BackgroundAlternate={} +BackgroundNormal={} +DecorationFocus={} +DecorationHover={} +ForegroundActive={} +ForegroundInactive={} +ForegroundLink={} +ForegroundNegative={} +ForegroundNeutral={} +ForegroundNormal={} +ForegroundPositive={} +ForegroundVisited={}"#, + to_rgb(colors.background_alternate, bg), + to_rgb(colors.background_normal, bg), + to_rgb(colors.decoration_focus, bg), + to_rgb(colors.decoration_hover, bg), + to_rgb(colors.foreground_active, bg), + to_rgb(colors.foreground_inactive, bg), + to_rgb(colors.foreground_link, bg), + to_rgb(colors.foreground_negative, bg), + to_rgb(colors.foreground_neutral, bg), + to_rgb(colors.foreground_normal, bg), + to_rgb(colors.foreground_positive, bg), + to_rgb(colors.foreground_visited, bg), + ) +} + +/// Sets the colors for the titlebars of active and inactive windows. +fn format_ini_wm_colors(view_colors: &IniColors, is_dark: bool) -> String { + let bg = view_colors.background_normal; + let fg = view_colors.foreground_active; + let blend = if is_dark { fg } else { bg }; + + format!( + r#"activeBackground={} +activeBlend={} +activeForeground={} +inactiveBackground={} +inactiveBlend={} +inactiveForeground={}"#, + to_rgb(bg, bg), + to_rgb(blend, bg), + to_rgb(fg, bg), + to_rgb(bg, bg), + to_rgb(blend, bg), + to_rgb(fg, bg), + ) +} + +struct IniColorEffects { + color: Srgba, + color_amount: f32, + color_effect: ColorEffect, + contrast_amount: f32, + /// Applied to the text, using the background as the reference color. + contrast_effect: ColorEffect, + intensity_amount: f32, + intensity_effect: IntensityEffect, +} +/// Each color set is made up of a number of roles which are available in all other sets. +/// In addition, except for Inactive Text, there is a corresponding background role for each of the text roles. Currently (except for Normal and Alternate Background), these colors are not chosen here but are automatically determined based on Normal Background and the corresponding Text color. +struct IniColors { + /// used when there is a need to subtly change the background to aid in item association. This might be used e.g. as the background of a heading, but is mostly used for alternating rows in lists, especially multi-column lists, to aid in visually tracking rows. + background_alternate: Srgba, + /// Normal background + background_normal: Srgba, + /// Used for drawing lines or shading UI elements to indicate the item which has active input focus. + /// Typically the same as foreground_active. + decoration_focus: Srgba, + /// Used for drawing lines or shading UI elements for mouse-over effects, e.g. the "illumination" effects for buttons. + /// Typically the same as foreground_active. + decoration_hover: Srgba, + /// used to indicate an active element or attract attention, e.g. alerts, notifications; also for hovered hyperlinks + foreground_active: Srgba, + /// used for text which should be unobtrusive, e.g. comments, "subtitles", unimportant information, etc. + foreground_inactive: Srgba, + /// used for hyperlinks or to otherwise indicate "something which may be visited", or to show relationships + foreground_link: Srgba, + /// used for errors, failure notices, notifications that an action may be dangerous (e.g. unsafe web page or security context), etc. + foreground_negative: Srgba, + /// used to draw attention when another role is not appropriate; e.g. warnings, to indicate secure/encrypted content, etc. + foreground_neutral: Srgba, + /// Normal foreground + foreground_normal: Srgba, + /// used for success notices, to indicate trusted content, etc. + foreground_positive: Srgba, + /// used for "something (e.g. a hyperlink) that has been visited", or to indicate something that is "old". + foreground_visited: Srgba, +} + +/// Intensity allows the overall color to be lightened or darkened. +#[allow(dead_code)] +enum IntensityEffect { + /// Makes everything lighter or darker in a controlled manner. + /// + /// intensity_amount increases or decreases the overall intensity (i.e. perceived brightness) by an absolute amount. + Shade, + /// Changes the intensity to a percentage of the initial value. + Darken, + /// Conceptually the opposite of darken; lighten can be thought of as working with "distance from white", where darken works with "distance from black". + Lighten, +} + +impl IntensityEffect { + pub fn as_u8(&self) -> u8 { + match self { + Self::Shade => 0, + Self::Darken => 1, + Self::Lighten => 2, + } + } +} + +/// This also changes the overall color like [IntensityEffect], +/// but is not limited to intensity. +#[allow(dead_code)] +enum ColorEffect { + /// changes the relative chroma + /// + /// This is available for "ColorEffect" but not "ContrastEffect". + Desaturate, + /// smoothly blends the original color into a reference color + Fade, + /// similar to Fade, except that the color (hue and chroma) changes more quickly while the intensity changes more slowly as the amount is increased + Tint, +} + +impl ColorEffect { + pub fn as_u8(&self) -> u8 { + match self { + Self::Desaturate => 0, + Self::Fade => 1, + Self::Tint => 2, + } + } +} From b05f040e5f0dc390af1847caf5f50b6813215795 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 16 Feb 2026 08:03:35 +0100 Subject: [PATCH 035/168] i18n: translation updates from weblate Co-authored-by: Benmak Kizuna Co-authored-by: Fedorov Alexei Co-authored-by: Hosted Weblate Co-authored-by: jonnysemon Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 2 +- i18n/ru/libcosmic.ftl | 19 +++++++++++++++++++ i18n/yue-Hant/libcosmic.ftl | 0 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 i18n/yue-Hant/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index ce3eb1e8..92f016f0 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -3,7 +3,7 @@ close = أغلِق # About license = الترخيص links = الروابط -developers = المطورون +developers = المطوِّرون designers = المصمّمون artists = الفنانون translators = المترجمون diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl index 0ef03fb1..7fe9b3dc 100644 --- a/i18n/ru/libcosmic.ftl +++ b/i18n/ru/libcosmic.ftl @@ -6,3 +6,22 @@ designers = Дизайнеры artists = Художники translators = Переводчики documenters = Авторы документации +january = Январь { $year } +february = Февраль { $year } +march = Март { $year } +april = Апрель { $year } +may = Май { $year } +june = Июнь { $year } +july = Июль { $year } +august = Август { $year } +september = Сентябрь { $year } +october = Октябрь { $year } +november = Ноябрь { $year } +december = Декабрь { $year } +monday = Пн +tuesday = Вт +wednesday = Ср +thursday = Чт +friday = Пт +saturday = Сб +sunday = Вс diff --git a/i18n/yue-Hant/libcosmic.ftl b/i18n/yue-Hant/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 990e2e291b33379f985bae0f81e46292b3d0e9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:32:21 +0100 Subject: [PATCH 036/168] refactor(calendar): use `jiff` instead of `chrono` This refactors the calendar widget to use `jiff` instead of `chrono`. Also mostly matches the design of the widget to the time applet. --- Cargo.toml | 2 +- examples/calendar/Cargo.toml | 6 +- examples/calendar/src/main.rs | 8 +- i18n/en/libcosmic.ftl | 21 ++-- src/widget/calendar.rs | 197 +++++++++++++++------------------- 5 files changed, 111 insertions(+), 123 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index feaa8c74..4aaf9d0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.7" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } -chrono = "0.4.43" +jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } # Internationalization diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index 59b23c0c..b7286825 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "calendar" -version = "0.1.0" -edition = "2021" +version = "1.0.0" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = "0.4.42" +jiff = "0.2" [dependencies.libcosmic] path = "../../" diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 589bc1ff..240684c6 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -3,10 +3,10 @@ //! Calendar widget example -use chrono::NaiveDate; use cosmic::app::{Core, Settings, Task}; use cosmic::widget::calendar::CalendarModel; -use cosmic::{executor, iced, ApplicationExt, Element}; +use cosmic::{ApplicationExt, Element, executor, iced}; +use jiff::civil::{Date, Weekday}; /// Runs application with these settings #[rustfmt::skip] @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { - DateSelected(NaiveDate), + DateSelected(Date), PrevMonth, NextMonth, } @@ -92,7 +92,7 @@ impl cosmic::Application for App { |date| Message::DateSelected(date), || Message::PrevMonth, || Message::NextMonth, - chrono::Weekday::Sun, + Weekday::Sunday, ); content = content.push(calendar); diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic.ftl index 119ac38e..257fc44f 100644 --- a/i18n/en/libcosmic.ftl +++ b/i18n/en/libcosmic.ftl @@ -23,10 +23,17 @@ september = September { $year } october = October { $year } november = November { $year } december = December { $year } -monday = Mon -tuesday = Tue -wednesday = Wed -thursday = Thu -friday = Fri -saturday = Sat -sunday = Sun +monday = Monday +mon = Mon +tuesday = Tuesday +tue = Tue +wednesday = Wednesday +wed = Wed +thursday = Thursday +thu = Thu +friday = Friday +fri = Fri +saturday = Saturday +sat = Sat +sunday = Sunday +sun = Sun diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 7ee06204..ea10fddb 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -3,19 +3,20 @@ //! A widget that displays an interactive calendar. -use std::cmp; - use crate::fl; -use crate::iced_core::{Alignment, Length, Padding}; -use crate::widget::{Grid, button, column, grid, icon, row, text}; +use crate::iced_core::{Alignment, Length}; +use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; -use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday}; use iced::alignment::Vertical; +use jiff::{ + ToSpan, + civil::{Date, Weekday}, +}; /// A widget that displays an interactive calendar. pub fn calendar( model: &CalendarModel, - on_select: impl Fn(NaiveDate) -> M + 'static, + on_select: impl Fn(Date) -> M + 'static, on_prev: impl Fn() -> M + 'static, on_next: impl Fn() -> M + 'static, first_day_of_week: Weekday, @@ -29,61 +30,40 @@ pub fn calendar( } } -pub fn set_day(date_selected: NaiveDate, day: u32) -> NaiveDate { - let current = date_selected.day(); - - let new_date = match current.cmp(&day) { - cmp::Ordering::Less => date_selected.checked_add_days(Days::new((day - current) as u64)), - - cmp::Ordering::Greater => date_selected.checked_sub_days(Days::new((current - day) as u64)), - - _ => None, - }; - - if let Some(new) = new_date { - new - } else { - date_selected - } +pub fn set_day(date_selected: Date, day: i8) -> Date { + date_selected + .with() + .day(day) + .build() + .unwrap_or(date_selected) } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] pub struct CalendarModel { - pub selected: NaiveDate, - pub visible: NaiveDate, + pub selected: Date, + pub visible: Date, } impl CalendarModel { pub fn now() -> Self { - let now = Local::now(); - let naive_now = NaiveDate::from(now.naive_local()); + let now = jiff::Zoned::now().date(); CalendarModel { - selected: naive_now, - visible: naive_now, + selected: now, + visible: now, } } #[inline] - pub fn new(selected: NaiveDate, visible: NaiveDate) -> Self { + pub fn new(selected: Date, visible: Date) -> Self { CalendarModel { selected, visible } } pub fn show_prev_month(&mut self) { - let prev_month_date = self - .visible - .checked_sub_months(Months::new(1)) - .expect("valid naivedate"); - - self.visible = prev_month_date; + self.visible = self.visible.checked_sub(1.month()).expect("valid date"); } pub fn show_next_month(&mut self) { - let next_month_date = self - .visible - .checked_add_months(Months::new(1)) - .expect("valid naivedate"); - - self.visible = next_month_date; + self.visible = self.visible.checked_add(1.month()).expect("valid date"); } #[inline] @@ -99,7 +79,7 @@ impl CalendarModel { } #[inline] - pub fn set_selected_visible(&mut self, selected: NaiveDate) { + pub fn set_selected_visible(&mut self, selected: Date) { self.selected = selected; self.visible = self.selected; } @@ -107,7 +87,7 @@ impl CalendarModel { pub struct Calendar<'a, M> { model: &'a CalendarModel, - on_select: Box M>, + on_select: Box M>, on_prev: Box M>, on_next: Box M>, first_day_of_week: Weekday, @@ -121,45 +101,57 @@ where macro_rules! translate_month { ($month:expr, $year:expr) => {{ match $month { - chrono::Month::January => fl!("january", year = $year), - chrono::Month::February => fl!("february", year = $year), - chrono::Month::March => fl!("march", year = $year), - chrono::Month::April => fl!("april", year = $year), - chrono::Month::May => fl!("may", year = $year), - chrono::Month::June => fl!("june", year = $year), - chrono::Month::July => fl!("july", year = $year), - chrono::Month::August => fl!("august", year = $year), - chrono::Month::September => fl!("september", year = $year), - chrono::Month::October => fl!("october", year = $year), - chrono::Month::November => fl!("november", year = $year), - chrono::Month::December => fl!("december", year = $year), + 1 => fl!("january", year = $year), + 2 => fl!("february", year = $year), + 3 => fl!("march", year = $year), + 4 => fl!("april", year = $year), + 5 => fl!("may", year = $year), + 6 => fl!("june", year = $year), + 7 => fl!("july", year = $year), + 8 => fl!("august", year = $year), + 9 => fl!("september", year = $year), + 10 => fl!("october", year = $year), + 11 => fl!("november", year = $year), + 12 => fl!("december", year = $year), + _ => unreachable!(), } }}; } macro_rules! translate_weekday { - ($weekday:expr) => {{ + ($weekday:expr, short) => {{ match $weekday { - Weekday::Mon => fl!("monday"), - Weekday::Tue => fl!("tuesday"), - Weekday::Wed => fl!("wednesday"), - Weekday::Thu => fl!("thursday"), - Weekday::Fri => fl!("friday"), - Weekday::Sat => fl!("saturday"), - Weekday::Sun => fl!("sunday"), + Weekday::Monday => fl!("mon"), + Weekday::Tuesday => fl!("tue"), + Weekday::Wednesday => fl!("wed"), + Weekday::Thursday => fl!("thu"), + Weekday::Friday => fl!("fri"), + Weekday::Saturday => fl!("sat"), + Weekday::Sunday => fl!("sun"), + } + }}; + ($weekday:expr, long) => {{ + match $weekday { + Weekday::Monday => fl!("monday"), + Weekday::Tuesday => fl!("tuesday"), + Weekday::Wednesday => fl!("wednesday"), + Weekday::Thursday => fl!("thursday"), + Weekday::Friday => fl!("friday"), + Weekday::Saturday => fl!("saturday"), + Weekday::Sunday => fl!("sunday"), } }}; } let date = text(translate_month!( - Month::try_from(this.model.visible.month() as u8) - .expect("Previously valid month is suddenly invalid"), + this.model.visible.month(), this.model.visible.year() )) .size(18); - let day = text::body(translate_weekday!(this.model.visible.weekday())); + let day = text::body(translate_weekday!(this.model.visible.weekday(), long)); let month_controls = row::with_capacity(2) + .spacing(8) .push( icon::from_name("go-previous-symbolic") .apply(button::icon) @@ -171,46 +163,49 @@ where .on_press((this.on_next)()), ); - // Calender - let mut calendar_grid: Grid<'_, Message> = - grid().padding([0, 12].into()).width(Length::Fill); + // Calendar + let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill); let mut first_day_of_week = this.first_day_of_week; for _ in 0..7 { calendar_grid = calendar_grid.push( - text(translate_weekday!(first_day_of_week)) - .size(12) - .width(Length::Fixed(36.0)) + text::caption(translate_weekday!(first_day_of_week, short)) + .width(Length::Fixed(44.0)) .align_x(Alignment::Center), ); - first_day_of_week = first_day_of_week.succ(); + first_day_of_week = first_day_of_week.next(); } calendar_grid = calendar_grid.insert_row(); - let monday = get_calender_first( + let first = get_calendar_first( this.model.visible.year(), this.model.visible.month(), - first_day_of_week, + this.first_day_of_week, ); - let mut day_iter = monday.iter_days(); + + let today = jiff::Zoned::now().date(); for i in 0..42 { if i > 0 && i % 7 == 0 { calendar_grid = calendar_grid.insert_row(); } - let date = day_iter.next().unwrap(); - let is_currently_viewed_month = date.month() == this.model.visible.month() - && date.year_ce() == this.model.visible.year_ce(); - let is_currently_selected_month = date.month() == this.model.selected.month() - && date.year_ce() == this.model.selected.year_ce(); + let date = first + .checked_add(i.days()) + .expect("valid date in calendar range"); + let is_currently_viewed_month = + date.first_of_month() == this.model.visible.first_of_month(); + let is_currently_selected_month = + date.first_of_month() == this.model.selected.first_of_month(); let is_currently_selected_day = date.day() == this.model.selected.day() && is_currently_selected_month; + let is_today = date == today; calendar_grid = calendar_grid.push(date_button( date, is_currently_viewed_month, is_currently_selected_day, + is_today, &this.on_select, )); } @@ -225,9 +220,8 @@ where .padding([12, 20]) .into(), calendar_grid.into(), - padded_control(crate::widget::divider::horizontal::default()).into(), ]) - .width(315) + .width(360) .padding([8, 0]); Self::new(content_list) @@ -235,21 +229,24 @@ where } fn date_button( - date: NaiveDate, + date: Date, is_currently_viewed_month: bool, is_currently_selected_day: bool, - on_select: &dyn Fn(NaiveDate) -> Message, + is_today: bool, + on_select: &dyn Fn(Date) -> Message, ) -> crate::widget::Button<'static, Message> { let style = if is_currently_selected_day { button::ButtonClass::Suggested + } else if is_today { + button::ButtonClass::Standard } else { button::ButtonClass::Text }; let button = button::custom(text(format!("{}", date.day())).center()) .class(style) - .height(Length::Fixed(36.0)) - .width(Length::Fixed(36.0)); + .height(Length::Fixed(44.0)) + .width(Length::Fixed(44.0)); if is_currently_viewed_month { button.on_press((on_select)(set_day(date, date.day()))) @@ -258,26 +255,10 @@ fn date_button( } } -/// Gets the first date that will be visible on the calender +/// Gets the first date that will be visible on the calendar #[must_use] -pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate { - let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); - let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; // chrono::Weekday.num_days_from - date.checked_sub_days(Days::new(num_days as u64)).unwrap() -} - -// TODO: Refactor to use same function from applet module. -fn padded_control<'a, Message>( - content: impl Into>, -) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> { - crate::widget::container(content) - .padding(menu_control_padding()) - .width(Length::Fill) -} - -#[inline] -fn menu_control_padding() -> Padding { - let guard = crate::theme::THEME.lock().unwrap(); - let cosmic = guard.cosmic(); - [cosmic.space_xxs(), cosmic.space_m()].into() +pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date { + let date = Date::new(year, month, 1).expect("valid date"); + let num_days = date.weekday().since(from_weekday); + date.checked_sub(num_days.days()).expect("valid date") } From cb288070af610e0858e0c5e1818dc4dee3dcc00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:37:56 +0100 Subject: [PATCH 037/168] chore: cargo fmt --- cosmic-theme/src/model/theme.rs | 4 +++- src/command.rs | 6 +++++- src/widget/dnd_destination.rs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index cef479ae..89d87b6b 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -692,7 +692,9 @@ impl Theme { let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; Self::get_active_with_brightness(is_dark) } - pub fn get_active_with_brightness(is_dark: bool) -> Result, Self)> { + pub fn get_active_with_brightness( + is_dark: bool, + ) -> Result, Self)> { let config = if is_dark { Self::dark_config() } else { diff --git a/src/command.rs b/src/command.rs index 14d326b4..00684e55 100644 --- a/src/command.rs +++ b/src/command.rs @@ -48,7 +48,11 @@ pub fn toggle_maximize(id: window::Id) -> iced::Task> { } #[cfg(feature = "xdg-portal")] -pub fn file_transfer_send(writeable: bool, auto_stop: bool, files: Vec) -> iced::Task> { +pub fn file_transfer_send( + writeable: bool, + auto_stop: bool, + files: Vec, +) -> iced::Task> { iced::Task::future(async move { let file_transfer = ashpd::documents::FileTransfer::new().await?; let key = file_transfer.start_transfer(writeable, auto_stop).await?; diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index a32a9fba..7225e917 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -522,7 +522,10 @@ impl Widget ); #[cfg(feature = "xdg-portal")] - if mime_type == FILE_TRANSFER_MIME && let Some(f) = self.on_file_transfer.as_ref() && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { + if mime_type == FILE_TRANSFER_MIME + && let Some(f) = self.on_file_transfer.as_ref() + && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) + { shell.publish(f(s)); return event::Status::Captured; } From be98b7dd6f618c28778cb7c0fb9982a96c8a36aa Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 18 Feb 2026 14:18:27 +0100 Subject: [PATCH 038/168] refactor(cosmic-theme): remove recently-added `Theme::get_active_with_brightness` The added method was not necessary. Also improves the code in the get_active method. --- cosmic-theme/src/model/theme.rs | 26 ++++++++++---------------- cosmic-theme/src/output/qt_output.rs | 7 ++++++- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 89d87b6b..8e1cd9f7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -685,23 +685,17 @@ impl Theme { self.shade } - /// get the active theme + /// Get the active theme based on the current theme mode. pub fn get_active() -> Result, Self)> { - let config = - Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?; - let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; - Self::get_active_with_brightness(is_dark) - } - pub fn get_active_with_brightness( - is_dark: bool, - ) -> Result, Self)> { - let config = if is_dark { - Self::dark_config() - } else { - Self::light_config() - } - .map_err(|e| (vec![e], Self::default()))?; - Self::get_entry(&config) + (|| { + (if ThemeMode::is_dark(&Config::new(Self::id(), Self::VERSION)?)? { + Self::dark_config + } else { + Self::light_config + })() + })() + .map_err(|error| (vec![error], Self::default())) + .and_then(|theme_config| Self::get_entry(&theme_config)) } #[must_use] diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 0d9a4258..78bdec61 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -1,5 +1,6 @@ use crate::Theme; use configparser::ini::Ini; +use cosmic_config::CosmicConfigEntry; use palette::{Mix, Srgba, blend::Compose}; use std::{ fs::{self, File}, @@ -92,7 +93,11 @@ impl Theme { let dark = if self.is_dark { self.clone() } else { - Self::get_active_with_brightness(false).unwrap_or_else(|_| self.clone()) + Theme::light_config() + .ok() + .as_ref() + .and_then(|conf| Theme::get_entry(conf).ok()) + .unwrap_or_else(|| self.clone()) }; IniColors { background_alternate: dark.accent.base, From 7c49a736ec628150fd656e50a24ad2541b28f3ef Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 18 Feb 2026 14:23:09 +0100 Subject: [PATCH 039/168] refactor(cosmic-theme): remove `Theme::apply_exports_static` Recently-added method is redundant with `apply_exports`, and the dark mode preference is already defined in the theme being applied. --- cosmic-theme/src/output/mod.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 61f0e49d..37271dbb 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -41,19 +41,6 @@ impl Theme { Ok(()) } - #[inline] - /// To avoid rewriting too much code, I replaced calls to `Theme::apply_gtk` with this. - /// Note that vscode isn't touched by this function. - pub fn apply_exports_static(is_dark: bool) -> Result<(), OutputError> { - let gtk_res = Theme::apply_gtk(is_dark); - let qt_res = Theme::apply_qt(is_dark); - let qt56ct_res = Theme::apply_qt56ct(is_dark); - gtk_res?; - qt_res?; - qt56ct_res?; - Ok(()) - } - #[inline] pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); From e1dad541b281bbc54f3f798380ccd4141a22d67c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 18 Feb 2026 14:59:14 +0100 Subject: [PATCH 040/168] chore(cosmic-theme): `Theme::apply_exports` should not apply VS Code theme currently --- cosmic-theme/src/output/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 37271dbb..0baefb86 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -29,19 +29,19 @@ pub enum OutputError { impl Theme { #[inline] + /// Apply COSMIC theme exports for GTK and Qt applications. pub fn apply_exports(&self) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(self.is_dark); let qt_res = Theme::apply_qt(self.is_dark); let qt56ct_res = Theme::apply_qt56ct(self.is_dark); - let vs_res = self.clone().apply_vs_code(); gtk_res?; qt_res?; qt56ct_res?; - vs_res?; Ok(()) } #[inline] + /// Write COSMIC theme exports for GTK and Qt applications. pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); let qt_res = self.write_qt(); @@ -51,6 +51,7 @@ impl Theme { } #[inline] + /// Un-export GTK and Qt theme configurations applied by us. pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); From dc3c194f09734256498b23b218b2c69f0eb21bf4 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 18 Feb 2026 20:02:58 +0000 Subject: [PATCH 041/168] fix(cosmic-theme): inverted Qt link_button colors --- cosmic-theme/src/output/qt_output.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 78bdec61..2e926a2e 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -48,7 +48,7 @@ impl Theme { decoration_hover: self.accent_text_color(), foreground_active: self.accent_text_color(), foreground_inactive: self.background.on.mix(bg, 0.1), - foreground_link: self.link_button.base, + foreground_link: self.link_button.on, foreground_negative: self.destructive_text_color(), foreground_neutral: self.warning_text_color(), foreground_normal: self.background.on, @@ -73,7 +73,7 @@ impl Theme { decoration_hover: selected, foreground_active: selected_text, foreground_inactive: selected_text.mix(selected, 0.5), - foreground_link: self.link_button.on, + foreground_link: self.link_button.base, foreground_negative: self.destructive_color(), foreground_neutral: self.warning_color(), foreground_normal: selected_text, @@ -106,7 +106,7 @@ impl Theme { decoration_hover: dark.accent_text_color(), foreground_active: dark.accent_text_color(), foreground_inactive: dark.background.on.mix(dark.background.base, 0.1), - foreground_link: dark.link_button.base, + foreground_link: dark.link_button.on, foreground_negative: dark.destructive_text_color(), foreground_neutral: dark.warning_text_color(), foreground_normal: dark.background.on, From 3ed5c173fd78e97b24fab0a059e5fe23b2f63b8f Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 18 Feb 2026 20:10:42 +0000 Subject: [PATCH 042/168] fix(cosmic-theme): copy for backup, not rename We're now merging the colors with kdeglobals, not replacing it with a symlink. So renaming the file gives us a missing file Io error: [2026-02-18T20:03:08Z ERROR cosmic_settings_daemon::theme] Failed to apply COSMIC theme exports. Io(Os { code: 2, kind: NotFound, message: "No such file or directory" }) --- cosmic-theme/src/output/qt_output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 2e926a2e..67ffbd69 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -338,7 +338,7 @@ widgetStyle=qt6ct-style fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> { if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) { let backup_path = path.with_extension("bak"); - fs::rename(path, &backup_path)?; + fs::copy(path, &backup_path)?; } Ok(()) } From 754b064bffbb399b955662d6c1fcca3468accb0c Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 18 Feb 2026 20:42:34 +0000 Subject: [PATCH 043/168] tweak(cosmic-theme): pretty write ini --- cosmic-theme/src/output/mod.rs | 7 +++++++ cosmic-theme/src/output/qt56ct_output.rs | 5 +++-- cosmic-theme/src/output/qt_output.rs | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 0baefb86..f331e8d2 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,3 +1,4 @@ +use configparser::ini::WriteOptions; use palette::{Srgba, rgb::Rgba}; use thiserror::Error; @@ -78,3 +79,9 @@ pub fn to_rgba(c: Srgba) -> String { c_u8.red, c_u8.green, c_u8.blue, c.alpha ) } + +pub fn qt_settings_ini_style() -> WriteOptions { + let mut write_options = WriteOptions::default(); + write_options.blank_lines_between_sections = 1; + write_options +} diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index d4736597..552e7fec 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -5,7 +5,7 @@ use std::{ path::PathBuf, }; -use super::OutputError; +use super::{OutputError, qt_settings_ini_style}; impl Theme { /// The "version" of this theme. @@ -86,7 +86,8 @@ impl Theme { } } - ini.write(path).map_err(OutputError::Io)?; + ini.pretty_write(path, &qt_settings_ini_style()) + .map_err(OutputError::Io)?; Ok(()) } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 67ffbd69..9bca3d18 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -8,7 +8,7 @@ use std::{ path::{Path, PathBuf}, }; -use super::OutputError; +use super::{OutputError, qt_settings_ini_style}; impl Theme { /// Produces a color scheme ini file for Qt. @@ -258,7 +258,7 @@ widgetStyle=qt6ct-style } kdeglobals_ini - .write(kdeglobals_file) + .pretty_write(kdeglobals_file, &qt_settings_ini_style()) .map_err(OutputError::Io)?; Ok(()) } From c1c09624bd5f46cdcfc042561070bab6faabaffc Mon Sep 17 00:00:00 2001 From: mariinkys Date: Thu, 19 Feb 2026 16:32:30 +0100 Subject: [PATCH 044/168] fix: right-clicking any sidebar item makes all sidebar items bold --- src/widget/segmented_button/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index e4f416bf..5201c908 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -246,7 +246,7 @@ where if let Some(text) = self.model.text.get(key) { let font = if self.button_is_focused(state, key) { self.font_active - } else if state.show_context.is_some() || self.button_is_hovered(state, key) { + } else if state.show_context == Some(key) || self.button_is_hovered(state, key) { self.font_hovered } else if self.model.is_active(key) { self.font_active From 1f6086e5ead97063bfc654f2ca9ad31eaf6944d3 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 19 Feb 2026 09:18:35 -0700 Subject: [PATCH 045/168] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index e2a24417..ecc29a83 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit e2a2441789a7e302f099c0e8e9493ef81b58e265 +Subproject commit ecc29a83982839f628e2ed1c01605c694a1fd3ac From b9bd773940950dc07b2cfa7c62c2588b0a653017 Mon Sep 17 00:00:00 2001 From: Hojjat Abdollahi Date: Thu, 19 Feb 2026 10:06:45 -0700 Subject: [PATCH 046/168] feat: ellipsize text (#1132) --- iced | 2 +- src/widget/dropdown/menu/mod.rs | 1 + src/widget/dropdown/multi/menu.rs | 2 ++ src/widget/dropdown/multi/widget.rs | 3 +++ src/widget/dropdown/widget.rs | 3 +++ src/widget/segmented_button/widget.rs | 4 +++- src/widget/text_input/input.rs | 7 +++++++ 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/iced b/iced index ecc29a83..d36e4df4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ecc29a83982839f628e2ed1c01605c694a1fd3ac +Subproject commit d36e4df47f2e277fafcd3505229d53438c7f128d diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 1d42d01f..3fd099b3 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -682,6 +682,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), color, diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 0035829f..39e89ee2 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -594,6 +594,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), color, @@ -643,6 +644,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), appearance.description_color, diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 458cf5e6..43a0836f 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -279,6 +279,7 @@ pub fn layout( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }); paragraph.min_width().round() }; @@ -423,6 +424,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }); paragraph.min_width().round() }; @@ -555,6 +557,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), style.text_color, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 03be4eb3..67101d26 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -212,6 +212,7 @@ where vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }); } @@ -478,6 +479,7 @@ pub fn layout( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }; let paragraph = match paragraph { Some(p) => { @@ -934,6 +936,7 @@ pub fn draw<'a, S>( vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), style.text_color, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 5201c908..1f009cc6 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -23,7 +23,7 @@ use iced::{ event, keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; -use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; +use iced_core::text::{Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; use iced_core::widget::{self, operation, tree}; use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text}; @@ -274,6 +274,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::None, + ellipsize: Ellipsize::None, line_height: self.line_height, }; @@ -602,6 +603,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), + ellipsize: Ellipsize::default(), line_height: self.line_height, }) }); diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 7dd92e12..e98d4cfa 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -728,6 +728,7 @@ where line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); let Size { width, height } = @@ -1160,6 +1161,7 @@ pub fn layout( line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); let label_size = label_paragraph.min_bounds(); @@ -1297,6 +1299,7 @@ pub fn layout( line_height: helper_text_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); let helper_text_size = helper_text_paragraph.min_bounds(); let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos); @@ -2260,6 +2263,7 @@ pub fn draw<'a, Message>( line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, label_layout.bounds().position(), appearance.label_color, @@ -2449,6 +2453,7 @@ pub fn draw<'a, Message>( line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, bounds.position(), color, @@ -2497,6 +2502,7 @@ pub fn draw<'a, Message>( line_height: helper_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, helper_text_layout.bounds().position(), text_color, @@ -2877,6 +2883,7 @@ fn replace_paragraph( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); } From 384e8f6e219bb458720eafa5bb971b832c057f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Mar=C3=ADn?= <62134857+mariinkys@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:06:45 +0100 Subject: [PATCH 047/168] fix(segmented_button): clear bold button text on context menu close --- src/widget/segmented_button/widget.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 1f009cc6..0e1af1d0 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2073,7 +2073,7 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - let state = tree.state.downcast_ref::(); + let state = tree.state.downcast_mut::(); let menu_state = state.menu_state.clone(); let entity = state.show_context?; @@ -2089,6 +2089,12 @@ where if !menu_state.inner.with_data(|data| data.open) { // If the menu is not open, we don't need to show it. + // We also clear the context entity and update the text + // cache so that the item is not bold when the context menu is closed + state.show_context = None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } return None; } bounds.x = state.context_cursor.x; From a37be90e811f365273bf632bf7090b63a368f092 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 9 Feb 2026 22:04:13 +0100 Subject: [PATCH 048/168] fix(single-instance): unminimize main window on dbus activate --- src/app/cosmic.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 803a56bd..bfda4a1d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1034,15 +1034,28 @@ impl Cosmic { } return Task::batch(cmds); } - Action::Activate(_token) => - { - #[cfg(feature = "wayland")] + Action::Activate(_token) => { if let Some(id) = self.app.core().main_window_id() { - return iced_winit::platform_specific::commands::activation::activate( - id, - #[allow(clippy::used_underscore_binding)] - _token, - ); + // Unminimize window before requesting to activate it. + let mut task = iced_runtime::window::minimize(id, false); + + #[cfg(feature = "wayland")] + { + task = task.chain( + iced_winit::platform_specific::commands::activation::activate( + id, + #[allow(clippy::used_underscore_binding)] + _token, + ), + ) + } + + #[cfg(not(feature = "wayland"))] + { + task = task.chain(iced_runtime::window::gain_focus(id)); + } + + return task; } } From f2caa66f0ff8e8fc1172d12742d08356fd55a78c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 2 Mar 2026 17:10:06 +0100 Subject: [PATCH 049/168] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Anonymous Co-authored-by: Arve Eriksson <031299870@telia.com> Co-authored-by: Baurzhan Muftakhidinov Co-authored-by: Benmak Kizuna Co-authored-by: David Carvalho Co-authored-by: Ettore Atalan Co-authored-by: Fedorov Alexei Co-authored-by: Feike Donia Co-authored-by: Geeson Wan Co-authored-by: Hosted Weblate Co-authored-by: Jiri Grönroos Co-authored-by: Julien Brouillard Co-authored-by: Marko X Co-authored-by: Tommi Nieminen Co-authored-by: VandaL Co-authored-by: Zahid Rizky Fakhri Co-authored-by: jonnysemon Co-authored-by: lorduskordus Co-authored-by: therealmate Co-authored-by: yakup Co-authored-by: Димко Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/cs/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fi/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fr/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hu/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/id/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kk/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sv/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/tr/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 7 +++++++ i18n/cs/libcosmic.ftl | 21 ++++++++++++++------- i18n/de/libcosmic.ftl | 23 ++++++++++++++--------- i18n/fi/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ i18n/fr/libcosmic.ftl | 21 ++++++++++++++------- i18n/ga/libcosmic.ftl | 21 ++++++++++++++------- i18n/hu/libcosmic.ftl | 21 ++++++++++++++------- i18n/id/libcosmic.ftl | 21 ++++++++++++++------- i18n/kk/libcosmic.ftl | 21 ++++++++++++++------- i18n/nl/libcosmic.ftl | 8 +++++++- i18n/pl/libcosmic.ftl | 21 ++++++++++++++------- i18n/pt-BR/libcosmic.ftl | 21 ++++++++++++++------- i18n/ru/libcosmic.ftl | 21 ++++++++++++++------- i18n/sl/libcosmic.ftl | 0 i18n/sv/libcosmic.ftl | 21 ++++++++++++++------- i18n/tr/libcosmic.ftl | 27 ++++++++++++++++++++++++++- i18n/uk/libcosmic.ftl | 21 ++++++++++++++------- i18n/zh-Hans/libcosmic.ftl | 21 ++++++++++++++------- 18 files changed, 256 insertions(+), 95 deletions(-) create mode 100644 i18n/sl/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index 92f016f0..35e6050f 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -27,3 +27,10 @@ thursday = الخميس friday = الجمعة saturday = السبت sunday = الأحد +mon = ن +tue = ث +wed = ر +thu = خ +fri = ج +sat = س +sun = ح diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic.ftl index 8f2ef348..850870d9 100644 --- a/i18n/cs/libcosmic.ftl +++ b/i18n/cs/libcosmic.ftl @@ -8,7 +8,7 @@ designers = Designéři artists = Grafici translators = Překladatelé documenters = Tvůrci dokumentace -sunday = Ne +sunday = Neděle january = Leden { $year } february = Únor { $year } march = Březen { $year } @@ -21,9 +21,16 @@ september = Září { $year } october = Říjen { $year } november = Listopad { $year } december = Prosinec { $year } -monday = Po -tuesday = Út -wednesday = St -thursday = Čt -friday = Pá -saturday = So +monday = Pondělí +tuesday = Úterý +wednesday = Středa +thursday = Čtvrtek +friday = Pátek +saturday = Sobota +mon = Po +tue = Út +wed = St +thu = Čt +fri = Pá +sat = So +sun = Ne diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 2ef7b765..1f17c924 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -3,11 +3,11 @@ close = Schließen # About license = Lizenz links = Links -developers = Entwickler*innen -designers = Designer*innen -artists = Künstler*innen +developers = Entwickler(innen) +designers = Designer(innen) +artists = Künstler(innen) translators = Übersetzer*innen -documenters = Dokumentierer*innen +documenters = Dokumentierer(innen) # Calendar january = Januar { $year } february = Februar { $year } @@ -23,8 +23,13 @@ november = November { $year } december = Dezember { $year } monday = Mo tuesday = Di -wednesday = Mi -thursday = Do -friday = Fr -saturday = Sa -sunday = So +wednesday = Mittwoch +thursday = Donnerstag +friday = Freitag +saturday = Samstag +sunday = Sonntag +wed = Mi +thu = Do +fri = Fr +sat = Sa +sun = So diff --git a/i18n/fi/libcosmic.ftl b/i18n/fi/libcosmic.ftl index e69de29b..877f225d 100644 --- a/i18n/fi/libcosmic.ftl +++ b/i18n/fi/libcosmic.ftl @@ -0,0 +1,34 @@ +monday = Maanantai +mon = ma +tuesday = Tiistai +tue = ti +wednesday = Keskiviikko +wed = ke +thursday = Torstai +thu = to +friday = Perjantai +fri = pe +saturday = Lauantai +sat = la +sunday = Sunnuntai +sun = su +close = Sulje +license = Lisenssi +links = Linkit +developers = Kehittäjät +designers = Suunnittelijat +artists = Artistit +translators = Kääntäjät +documenters = Dokumentoijat +january = Tammikuu { $year } +february = Helmikuu { $year } +march = Maaliskuu { $year } +april = Huhtikuu { $year } +may = Toukokuu { $year } +june = Kesäkuu { $year } +july = Heinäkuu { $year } +august = Elokuu { $year } +september = Syyskuu { $year } +october = Lokakuu { $year } +november = Marraskuu { $year } +december = Joulukuu { $year } diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic.ftl index 43e2d6f7..1ec6c0cf 100644 --- a/i18n/fr/libcosmic.ftl +++ b/i18n/fr/libcosmic.ftl @@ -10,18 +10,25 @@ february = Février { $year } april = Avril { $year } march = Mars { $year } november = Novembre { $year } -friday = Ven -tuesday = Mar +friday = Vendredi +tuesday = Mardi may = Mai { $year } -wednesday = Mer -monday = Lun +wednesday = Mercredi +monday = Lundi december = Décembre { $year } -sunday = Dim +sunday = Dimanche june = Juin { $year } -saturday = Sam +saturday = Samedi august = Août { $year } july = Juillet { $year } -thursday = Jeu +thursday = Jeudi september = Septembre { $year } october = Octobre { $year } designers = Designers +mon = Lun +tue = Mar +wed = Mer +thu = Jeu +fri = Ven +sat = Sam +sun = Dim diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic.ftl index 024841bf..bdf38d20 100644 --- a/i18n/ga/libcosmic.ftl +++ b/i18n/ga/libcosmic.ftl @@ -18,10 +18,17 @@ september = Meán Fómhair { $year } october = Deireadh Fómhair { $year } november = Samhain { $year } december = Nollaig { $year } -monday = Lua -tuesday = Mái -wednesday = Céa -thursday = Déa -friday = Aoi -saturday = Sat -sunday = Dom +monday = Dé Luain +tuesday = Dé Máirt +wednesday = Dé Céadaoin +thursday = Déardaoin +friday = Dé hAoine +saturday = Dé Sathairn +sunday = Dé Domhnaigh +mon = Lua +tue = Mái +wed = Céa +thu = Déa +fri = Aoi +sat = Sat +sun = Dom diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl index 02069244..7ff046b3 100644 --- a/i18n/hu/libcosmic.ftl +++ b/i18n/hu/libcosmic.ftl @@ -20,10 +20,17 @@ september = { $year } szeptember october = { $year } október november = { $year } november december = { $year } december -monday = H -tuesday = K -wednesday = Sze -thursday = Cs -friday = P -saturday = Szo -sunday = V +monday = Hétfő +tuesday = Kedd +wednesday = Szerda +thursday = Csütörtök +friday = Péntek +saturday = Szombat +sunday = Vasárnap +mon = H +tue = K +wed = Sze +thu = Cs +fri = P +sat = Szo +sun = V diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic.ftl index 2ce82dab..53e7736b 100644 --- a/i18n/id/libcosmic.ftl +++ b/i18n/id/libcosmic.ftl @@ -18,10 +18,17 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = Desember { $year } -monday = Sen -tuesday = Sel -wednesday = Rab -sunday = Min -saturday = Sab -friday = Jum -thursday = Kam +monday = Senin +tuesday = Selasa +wednesday = Rabu +sunday = Minggu +saturday = Sabtu +friday = Jum'at +thursday = Kamis +mon = Sen +tue = Sel +wed = Rab +thu = Kam +fri = Jum +sat = Sab +sun = Min diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl index bb06e98f..9d257114 100644 --- a/i18n/kk/libcosmic.ftl +++ b/i18n/kk/libcosmic.ftl @@ -18,10 +18,17 @@ september = Қыркүйек { $year } october = Қазан { $year } november = Қараша { $year } december = Желтоқсан { $year } -monday = Дс -tuesday = Сс -wednesday = Ср -thursday = Бс -friday = Жм -saturday = Сб -sunday = Жс +monday = Дүйсенбі +tuesday = Сейсенбі +wednesday = Сәрсенбі +thursday = Бейсенбі +friday = Жұма +saturday = Сенбі +sunday = Жексенбі +mon = Дс +tue = Сс +wed = Ср +thu = Бс +fri = Жм +sat = Сн +sun = Жк diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic.ftl index 75fc8cdf..7676b811 100644 --- a/i18n/nl/libcosmic.ftl +++ b/i18n/nl/libcosmic.ftl @@ -18,4 +18,10 @@ wednesday = Woe thursday = Do friday = Vrij saturday = Za -sunday = Zon +sunday = Zo +links = Links +developers = Ontwikkeling +designers = Ontwerp +translators = Vertaling +documenters = Documentatie +artists = Vormgeving diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic.ftl index 4bbfd67f..0d1649d4 100644 --- a/i18n/pl/libcosmic.ftl +++ b/i18n/pl/libcosmic.ftl @@ -20,10 +20,17 @@ september = Wrzesień { $year } october = Październik { $year } november = Listopad { $year } december = Grudzień { $year } -monday = Pon -tuesday = Wto -wednesday = Śro -thursday = Czw -friday = Pią -saturday = Sob -sunday = Nie +monday = Poniedziałek +tuesday = Wtorek +wednesday = Środa +thursday = Czwartek +friday = Piątek +saturday = Sobota +sunday = Niedziela +mon = Pon +tue = Wto +wed = Śro +thu = Czw +fri = Pia +sat = Sob +sun = Nie diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl index 51b5f6c3..1a51c799 100644 --- a/i18n/pt-BR/libcosmic.ftl +++ b/i18n/pt-BR/libcosmic.ftl @@ -20,10 +20,17 @@ september = Setembro de { $year } october = Outubro de { $year } november = Novembro de { $year } december = Dezembro de { $year } -monday = Seg -tuesday = Ter -wednesday = Qua -thursday = Qui -friday = Sex -saturday = Sáb -sunday = Dom +monday = Segunda-feira +tuesday = Terça-feira +wednesday = Quarta-feira +thursday = Quinta-feira +friday = Sexta-feira +saturday = Sábado +sunday = Domingo +mon = Seg +tue = Ter +wed = Qua +thu = Qui +fri = Sex +sat = Sáb +sun = Dom diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl index 7fe9b3dc..1ff78655 100644 --- a/i18n/ru/libcosmic.ftl +++ b/i18n/ru/libcosmic.ftl @@ -18,10 +18,17 @@ september = Сентябрь { $year } october = Октябрь { $year } november = Ноябрь { $year } december = Декабрь { $year } -monday = Пн -tuesday = Вт -wednesday = Ср -thursday = Чт -friday = Пт -saturday = Сб -sunday = Вс +monday = Понедельник +tuesday = Вторник +wednesday = Среда +thursday = Четверг +friday = Пятница +saturday = Суббота +sunday = Воскресенье +mon = Пн +tue = Вт +wed = Ср +thu = Чт +fri = Пт +sat = Сб +sun = Вс diff --git a/i18n/sl/libcosmic.ftl b/i18n/sl/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic.ftl index f0c647a1..27cdb393 100644 --- a/i18n/sv/libcosmic.ftl +++ b/i18n/sv/libcosmic.ftl @@ -18,10 +18,17 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = December { $year } -monday = Mån -tuesday = Tis -wednesday = Ons -thursday = Tor -friday = Fre -saturday = Lör -sunday = Sön +monday = Måndag +tuesday = Tisdag +wednesday = Onsdag +thursday = Torsdag +friday = Fredag +saturday = Lördag +sunday = Söndag +sun = Sön +mon = Mån +tue = Tis +wed = Ons +thu = Tor +fri = Fre +sat = Lör diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic.ftl index fd0f5475..39690200 100644 --- a/i18n/tr/libcosmic.ftl +++ b/i18n/tr/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Kapat - # About license = Lisans links = Bağlantılar @@ -9,3 +8,29 @@ designers = Tasarımcılar artists = Sanatçılar translators = Çevirmenler documenters = Belgelendiriciler +january = Ocak { $year } +february = Şubat { $year } +march = Mart { $year } +april = Nisan { $year } +may = Mayıs { $year } +june = Haziran { $year } +july = Temmuz { $year } +august = Ağustos { $year } +september = Eylül { $year } +october = Ekim { $year } +november = Kasım { $year } +december = Aralık { $year } +monday = Pazartesi +mon = Pzt +tuesday = Salı +tue = Sal +wednesday = Çarşamba +wed = Çar +thursday = Perşembe +thu = Per +friday = Cuma +fri = Cum +saturday = Cumartesi +sat = Cmt +sunday = Pazar +sun = Paz diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index d82c2a6e..cbe1cfaf 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -10,20 +10,27 @@ translators = Перекладачі documenters = Документатори february = Лютий { $year } november = Листопад { $year } -friday = Пт -tuesday = Вт +friday = П'ятниця +tuesday = Вівторок may = Травень { $year } -wednesday = Ср +wednesday = Середа april = Квітень { $year } -monday = Пн +monday = Понеділок december = Грудень { $year } -sunday = Нд +sunday = Неділя march = Березень { $year } june = Червень { $year } -saturday = Сб +saturday = Субота august = Серпень { $year } july = Липень { $year } -thursday = Чт +thursday = Четвер september = Вересень { $year } october = Жовтень { $year } january = Січень { $year } +mon = Пн +tue = Вт +wed = Ср +thu = Чт +fri = Пт +sat = Cб +sun = Нд diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index 9dfd6139..42330dcb 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -16,12 +16,19 @@ september = { $year }年9月 october = { $year }年10月 november = { $year }年11月 december = { $year }年12月 -monday = 周一 -tuesday = 周二 -wednesday = 周三 -thursday = 周四 -friday = 周五 -saturday = 周六 -sunday = 周日 +monday = 星期一 +tuesday = 星期二 +wednesday = 星期三 +thursday = 星期四 +friday = 星期五 +saturday = 星期六 +sunday = 星期日 artists = 艺术家 documenters = 文档作者 +mon = 周一 +tue = 周二 +wed = 周三 +thu = 周四 +fri = 周五 +sat = 周六 +sun = 周日 From bd1d3d5a73a858443c920ed8f83607846650a3e6 Mon Sep 17 00:00:00 2001 From: Hojjat Abdollahi Date: Mon, 2 Mar 2026 12:01:19 -0700 Subject: [PATCH 050/168] fix: ellipsize headerbar title instead of wrapping (#1140) --- src/widget/header_bar.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index c5bde28f..b0957d68 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -445,6 +445,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { std::mem::swap(&mut title, &mut self.title); widget::text::heading(title) + .wrapping(iced_core::text::Wrapping::None) + .ellipsize(iced_core::text::Ellipsize::End( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) .apply(widget::container) .center(Length::FillPortion(title_portion)) .into() From 85c27a99604f343bfa0fb2d78e6726f511f6a57a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 3 Mar 2026 21:18:45 +0100 Subject: [PATCH 051/168] fix(cosmic-theme): on reset of theme exports, do not remove VS code configs Closes #1139 --- cosmic-theme/src/output/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index f331e8d2..b2474dc1 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -56,10 +56,8 @@ impl Theme { pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); - let vs_res = Theme::reset_vs_code(); gtk_res?; qt_res?; - vs_res?; Ok(()) } } From 86dcf8af6cfcb3ab65e41649e4793f47c5595433 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 3 Mar 2026 23:32:00 +0100 Subject: [PATCH 052/168] feat(cosmic-icons): new icons for cosmic image viewer app --- cosmic-icons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-icons b/cosmic-icons index 70b07582..52520957 160000 --- a/cosmic-icons +++ b/cosmic-icons @@ -1 +1 @@ -Subproject commit 70b07582e24ec2114672256b9657ca80670bca8a +Subproject commit 5252095787cc96e2aed64604158f94e450703455 From e10459fb375d6a84e4ad296df98b1af610fcc531 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 10 Feb 2026 15:37:41 -0500 Subject: [PATCH 053/168] wip rebase updates --- Cargo.toml | 21 +- cosmic-config/src/dbus.rs | 335 ++++++++++--------- cosmic-config/src/subscription.rs | 51 ++- examples/application/Cargo.toml | 1 + iced | 2 +- src/app/action.rs | 2 - src/app/cosmic.rs | 15 +- src/app/mod.rs | 96 ++++-- src/app/multi_window.rs | 244 -------------- src/applet/column.rs | 46 +-- src/applet/mod.rs | 37 +- src/applet/row.rs | 46 +-- src/applet/token/subscription.rs | 7 +- src/command.rs | 2 +- src/dbus_activation.rs | 121 +++---- src/executor/multi.rs | 4 + src/executor/single.rs | 4 + src/theme/portal.rs | 9 +- src/theme/style/iced.rs | 158 +++++++-- src/widget/about.rs | 4 +- src/widget/aspect_ratio.rs | 20 +- src/widget/autosize.rs | 31 +- src/widget/button/widget.rs | 89 +++-- src/widget/calendar.rs | 4 +- src/widget/color_picker/mod.rs | 64 ++-- src/widget/context_drawer/overlay.rs | 29 +- src/widget/context_drawer/widget.rs | 23 +- src/widget/context_menu.rs | 22 +- src/widget/dialog.rs | 6 +- src/widget/dnd_destination.rs | 108 +++--- src/widget/dnd_source.rs | 65 ++-- src/widget/dropdown/menu/mod.rs | 81 +++-- src/widget/dropdown/mod.rs | 16 +- src/widget/dropdown/multi/menu.rs | 63 ++-- src/widget/dropdown/multi/widget.rs | 61 ++-- src/widget/dropdown/operation.rs | 96 +++--- src/widget/dropdown/widget.rs | 111 +++--- src/widget/flex_row/layout.rs | 10 +- src/widget/flex_row/widget.rs | 37 +- src/widget/frames.rs | 70 ++-- src/widget/grid/layout.rs | 14 +- src/widget/grid/widget.rs | 37 +- src/widget/header_bar.rs | 27 +- src/widget/icon/mod.rs | 17 +- src/widget/id_container.rs | 29 +- src/widget/layer_container.rs | 18 +- src/widget/list/column.rs | 4 +- src/widget/menu/flex.rs | 34 +- src/widget/menu/menu_bar.rs | 39 +-- src/widget/menu/menu_inner.rs | 124 +++---- src/widget/menu/menu_tree.rs | 29 +- src/widget/mod.rs | 18 +- src/widget/nav_bar.rs | 1 + src/widget/popover.rs | 87 +++-- src/widget/radio.rs | 31 +- src/widget/rectangle_tracker/mod.rs | 18 +- src/widget/rectangle_tracker/subscription.rs | 8 +- src/widget/responsive_container.rs | 31 +- src/widget/segmented_button/widget.rs | 164 +++++---- src/widget/settings/item.rs | 5 +- src/widget/spin_button.rs | 1 + src/widget/table/widget/compact.rs | 1 + src/widget/table/widget/standard.rs | 1 + src/widget/text_input/input.rs | 230 +++++++------ src/widget/toaster/widget.rs | 53 +-- src/widget/warning.rs | 1 + src/widget/wayland/tooltip/widget.rs | 65 ++-- src/widget/wrapper.rs | 22 +- 68 files changed, 1776 insertions(+), 1544 deletions(-) delete mode 100644 src/app/multi_window.rs diff --git a/Cargo.toml b/Cargo.toml index 4aaf9d0a..62b8ee7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,27 @@ rust-version = "1.90" name = "cosmic" [features] -default = ["dbus-config", "multi-window", "a11y"] +# default = ["dbus-config", "multi-window", "a11y"] +default = [ "debug", + "winit", + "tokio", + # "xdg-portal", + "a11y", + "wgpu", + "single-instance", + "surface-message", + "dbus-config", + "x11", + "wayland", + "multi-window", + "about","animated-image","autosize", "dbus-config", "pipewire", "process", "rfd", "desktop", "desktop-systemd-scope", "serde-keycode", "qr_code", "markdown", "highlighter" +] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget about = [] # Builds support for animated images -animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"] +animated-image = ["dep:async-fs", "image/gif", "image/webp", "image/png", "tokio?/io-util", "tokio?/fs"] # XXX autosize should not be used on winit windows unless dialogs autosize = [] applet = [ @@ -76,7 +90,7 @@ wayland = [ ] surface-message = [] # multi-window support -multi-window = ["iced/multi-window"] +multi-window = [] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit @@ -96,6 +110,7 @@ async-std = [ "zbus?/async-io", "iced/async-std", ] +x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index e9e3395c..da7bcb68 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,11 +1,11 @@ -use std::ops::Deref; +use std::{any::TypeId, ops::Deref}; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::{ Subscription, - futures::{self, Stream, StreamExt, future::pending}, + futures::{self, StreamExt, future::pending}, stream, }; @@ -57,6 +57,20 @@ impl Watcher { } } +#[derive(Clone)] +struct Wrapper( + TypeId, + CosmicSettingsDaemonProxy<'static>, + &'static str, + bool, +); + +impl std::hash::Hash for Wrapper { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + #[allow(clippy::too_many_lines)] pub fn watcher_subscription( settings_daemon: CosmicSettingsDaemonProxy<'static>, @@ -64,166 +78,185 @@ pub fn watcher_subscription iced_futures::Subscription> { let id = std::any::TypeId::of::(); - Subscription::run_with_id( - (id, config_id), - watcher_stream(settings_daemon, config_id, is_state), - ) -} + Subscription::run_with( + Wrapper(id, settings_daemon, config_id, is_state), + |&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| { + let is_state = *is_state; + let config_id = *config_id; + let settings_daemon = settings_daemon.clone(); + enum Change { + Changes(Changed), + OwnerChanged(bool), + } + stream::channel( + 5, + move |mut tx: futures::channel::mpsc::Sender>| async move { + let version = T::VERSION; -fn watcher_stream( - settings_daemon: CosmicSettingsDaemonProxy<'static>, - config_id: &'static str, - is_state: bool, -) -> impl Stream> { - enum Change { - Changes(Changed), - OwnerChanged(bool), - } - stream::channel(5, move |mut tx| async move { - let version = T::VERSION; + let Ok(cosmic_config) = (if is_state { + crate::Config::new_state(config_id, version) + } else { + crate::Config::new(config_id, version) + }) else { + pending::<()>().await; + unreachable!(); + }; - let Ok(cosmic_config) = (if is_state { - crate::Config::new_state(config_id, version) - } else { - crate::Config::new(config_id, version) - }) else { - pending::<()>().await; - unreachable!(); - }; + let mut attempts = 0; - let mut attempts = 0; + loop { + let watcher = if is_state { + Watcher::new_state(&settings_daemon, config_id, version).await + } else { + Watcher::new_config(&settings_daemon, config_id, version).await + }; + let Ok(watcher) = watcher else { + tracing::error!("Failed to create watcher for {config_id}"); - loop { - let watcher = if is_state { - Watcher::new_state(&settings_daemon, config_id, version).await - } else { - Watcher::new_config(&settings_daemon, config_id, version).await - }; - let Ok(watcher) = watcher else { - tracing::error!("Failed to create watcher for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; + let Ok(changes) = watcher.receive_changed().await else { + tracing::error!("Failed to listen for changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let Ok(changes) = watcher.receive_changed().await else { - tracing::error!("Failed to listen for changes for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; + let mut changes = changes.map(Change::Changes).fuse(); - let mut changes = changes.map(Change::Changes).fuse(); + let Ok(owner_changed) = watcher.inner().receive_owner_changed().await + else { + tracing::error!("Failed to listen for owner changes for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; + let mut owner_changed = owner_changed + .map(|c| Change::OwnerChanged(c.is_some())) + .fuse(); - let Ok(owner_changed) = watcher.inner().receive_owner_changed().await else { - tracing::error!("Failed to listen for owner changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let mut owner_changed = owner_changed - .map(|c| Change::OwnerChanged(c.is_some())) - .fuse(); + // update now, just in case we missed changes while setting up stream + let mut config = match T::get_entry(&cosmic_config) { + Ok(config) => config, + Err((errors, default)) => { + for why in &errors { + if why.is_err() { + if let crate::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { + // No system default config installed; don't error + continue; + } + } + tracing::error!("error getting config: {config_id} {why}"); + } + } + default + } + }; - // update now, just in case we missed changes while setting up stream - let mut config = match T::get_entry(&cosmic_config) { - Ok(config) => config, - Err((errors, default)) => { - for why in &errors { - if why.is_err() { - if let crate::Error::GetKey(_, err) = &why { - if err.kind() == std::io::ErrorKind::NotFound { - // No system default config installed; don't error - continue; + if let Err(err) = tx + .send(Update { + errors: Vec::new(), + keys: Vec::new(), + config: config.clone(), + }) + .await + { + tracing::error!("Failed to send config: {err}"); + } + + loop { + let change: Changed = futures::select! { + c = changes.next() => { + let Some(Change::Changes(c)) = c else { + break; + }; + c + } + c = owner_changed.next() => { + let Some(Change::OwnerChanged(cont)) = c else { + break; + }; + if cont { + continue; + } else { + // The settings daemon has exited + break; + } + }, + }; + + // Reset the attempts counter if we received a change + attempts = 0; + let Ok(args) = change.args() else { + // The settings daemon has exited + break; + }; + let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); + if !keys.is_empty() { + if let Err(err) = tx + .send(Update { + errors, + keys, + config: config.clone(), + }) + .await + { + tracing::error!("Failed to send config update: {err}"); } } - tracing::error!("error getting config: {config_id} {why}"); } } - default - } - }; - - if let Err(err) = tx - .send(Update { - errors: Vec::new(), - keys: Vec::new(), - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config: {err}"); - } - - loop { - let change: Changed = futures::select! { - c = changes.next() => { - let Some(Change::Changes(c)) = c else { - break; - }; - c - } - c = owner_changed.next() => { - let Some(Change::OwnerChanged(cont)) = c else { - break; - }; - if cont { - continue; - } else { - // The settings daemon has exited - break; - } - }, - }; - - // Reset the attempts counter if we received a change - attempts = 0; - let Ok(args) = change.args() else { - // The settings daemon has exited - break; - }; - let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); - if !keys.is_empty() { - if let Err(err) = tx - .send(Update { - errors, - keys, - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config update: {err}"); - } - } - } - } - }) + }, + ) + }, + ) } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 45e021fe..d16b9b65 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -25,7 +25,24 @@ pub fn config_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false)) + iced_futures::Subscription::run_with( + (id, config_id, config_version, false), + // FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function... + |(_, config_id, config_version, is_state)| { + let config_id = config_id.clone(); + let config_version = *config_version; + let is_state = *is_state; + + stream::channel(100, move |mut output| async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, is_state); + + loop { + state = start_listening::(state, &mut output).await; + } + }) + }, + ) } #[cold] @@ -37,25 +54,23 @@ pub fn config_state_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true)) -} - -fn watcher_stream( - config_id: Cow<'static, str>, - config_version: u64, - is_state: bool, -) -> impl Stream> { - stream::channel(100, move |mut output| { - let config_id = config_id.clone(); - async move { + iced_futures::Subscription::run_with( + (id, config_id, config_version, true), + |(_, config_id, config_version, is_state)| { let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, is_state); + let config_version = *config_version; + let is_state = *is_state; - loop { - state = start_listening::(state, &mut output).await; - } - } - }) + stream::channel(100, move |mut output| async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, is_state); + + loop { + state = start_listening::(state, &mut output).await; + } + }) + }, + ) } async fn start_listening( diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index f05c0418..35ff3d30 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -23,4 +23,5 @@ features = [ "wgpu", "single-instance", "surface-message", + "multi-window", ] diff --git a/iced b/iced index d36e4df4..73369a18 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d36e4df47f2e277fafcd3505229d53438c7f128d +Subproject commit 73369a18eb4069f3f3d1916fd1e17537ee87a587 diff --git a/src/app/action.rs b/src/app/action.rs index cbdd1a55..05fc7cbe 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -8,8 +8,6 @@ use crate::{config::CosmicTk, keyboard_nav}; #[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland")))] -use iced::Application as IcedApplication; /// A message managed internally by COSMIC. #[derive(Clone, Debug)] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index bfda4a1d..edd7b157 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -15,7 +15,7 @@ use cosmic_theme::ThemeMode; use iced::Application as IcedApplication; #[cfg(feature = "wayland")] use iced::event::wayland; -use iced::{Task, window}; +use iced::{Task, theme, window}; use iced_futures::event::listen_with; #[cfg(feature = "wayland")] use iced_winit::SurfaceIdWrapper; @@ -397,15 +397,16 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance { + pub fn style(&self, theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style } else if self.app.core().window.is_maximized { let theme = THEME.lock().unwrap(); - crate::style::iced::application::appearance(theme.borrow()) + crate::style::iced::application::style(theme.borrow()) } else { let theme = THEME.lock().unwrap(); - iced_runtime::Appearance { + + theme::Style { background_color: iced_core::Color::TRANSPARENT, icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), @@ -635,7 +636,7 @@ impl Cosmic { self.app.on_window_resize(id, width, height); //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) - return iced::window::get_maximized(id).map(move |maximized| { + return iced::window::is_maximized(id).map(move |maximized| { crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) }); } @@ -711,10 +712,10 @@ impl Cosmic { Action::KeyboardNav(message) => match message { keyboard_nav::Action::FocusNext => { - return iced::widget::focus_next().map(crate::Action::Cosmic); + return iced::widget::operation::focus_next().map(crate::Action::Cosmic); } keyboard_nav::Action::FocusPrevious => { - return iced::widget::focus_previous().map(crate::Action::Cosmic); + return iced::widget::operation::focus_previous().map(crate::Action::Cosmic); } keyboard_nav::Action::Escape => return self.app.on_escape(), keyboard_nav::Action::Search => return self.app.on_search(), diff --git a/src/app/mod.rs b/src/app/mod.rs index 67636dac..1287dc27 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,9 +11,8 @@ pub use action::Action; use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; +use iced::application::BootFn; pub mod cosmic; -#[cfg(all(feature = "winit", feature = "multi-window"))] -pub(crate) mod multi_window; pub mod settings; pub type Task = iced::Task>; @@ -21,12 +20,13 @@ pub type Task = iced::Task>; pub use crate::Core; use crate::prelude::*; use crate::theme::THEME; -use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover}; +use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::window; use iced::{Length, Subscription}; +use iced::{theme, window}; pub use settings::Settings; use std::borrow::Cow; +use std::{cell::RefCell, rc::Rc}; #[cold] pub(crate) fn iced_settings( @@ -72,7 +72,7 @@ pub(crate) fn iced_settings( core.exit_on_main_window_closed = exit_on_close; if let Some(border_size) = settings.resizable { - window_settings.resize_border = border_size as u32; + // window_settings.resize_border = border_size as u32; window_settings.resizable = true; } window_settings.decorations = !settings.client_decorations; @@ -82,7 +82,7 @@ pub(crate) fn iced_settings( window_settings.min_size = Some(min_size); } let max_size = settings.size_limits.max(); - if max_size != iced::Size::INFINITY { + if max_size != iced::Size::INFINITE { window_settings.max_size = Some(max_size); } @@ -90,6 +90,22 @@ pub(crate) fn iced_settings( (iced, (core, flags), window_settings) } +pub(crate) struct BootDataInner { + pub flags: A::Flags, + pub core: Core, +} + +pub(crate) struct BootData(pub Rc>>>); + +impl BootFn, crate::Action> + for BootData +{ + fn boot(&self) -> (cosmic::Cosmic, iced::Task>) { + let mut data = self.0.borrow_mut(); + let data = data.take().unwrap(); + cosmic::Cosmic::::init((data.core, data.flags)) + } +} /// Launch a COSMIC application with the given [`Settings`]. /// /// # Errors @@ -102,39 +118,50 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res } let default_font = settings.default_font; - let (settings, mut flags, window_settings) = iced_settings::(settings, flags); + let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { - flags.0.main_window = Some(iced::window::Id::RESERVED); + core.main_window = Some(iced::window::Id::RESERVED); + iced::application( - cosmic::Cosmic::title, + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ) .subscription(cosmic::Cosmic::subscription) + .title(cosmic::Cosmic::title) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } #[cfg(feature = "multi-window")] { - let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // app = app.window(window_settings); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if flags.0.main_window.is_none() { - app = app.window(window_settings); - flags.0.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) + .title(cosmic::Cosmic::title) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .settings(settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } } @@ -204,13 +231,16 @@ where tracing::info!("Another instance is running"); Ok(()) } else { - let (settings, mut flags, window_settings) = iced_settings::(settings, flags); - flags.0.single_instance = true; + let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); + core.single_instance = true; #[cfg(not(feature = "multi-window"))] { iced::application( - cosmic::Cosmic::title, + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ) @@ -220,24 +250,30 @@ where .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } #[cfg(feature = "multi-window")] { - let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // app = app.window(window_settings); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if flags.0.main_window.is_none() { - app = app.window(window_settings); - flags.0.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) .style(cosmic::Cosmic::style) + .title(cosmic::Cosmic::title) .theme(cosmic::Cosmic::theme) .settings(settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } } } @@ -428,7 +464,7 @@ where } /// Overrides the default style for applications - fn style(&self) -> Option { + fn style(&self) -> Option { None } @@ -667,7 +703,7 @@ impl ApplicationExt for App { ) } else { //TODO: this element is added to workaround state issues - widgets.push(horizontal_space().width(Length::Shrink).into()); + widgets.push(space::horizontal().width(Length::Shrink).into()); } } } diff --git a/src/app/multi_window.rs b/src/app/multi_window.rs deleted file mode 100644 index 65ac61f7..00000000 --- a/src/app/multi_window.rs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Create and run daemons that run in the background. -//! Copied from iced 0.13, but adds optional initial window - -use iced::application; -use iced::window; -use iced::{ - self, Program, - program::{self, with_style, with_subscription, with_theme, with_title}, - runtime::{Appearance, DefaultStyle}, -}; -use iced::{Element, Result, Settings, Subscription, Task}; - -use std::marker::PhantomData; - -pub(crate) struct Instance { - update: Update, - view: View, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - _executor: PhantomData, -} - -/// Creates an iced [`MultiWindow`] given its title, update, and view logic. -pub fn multi_window( - title: impl Title, - update: impl application::Update, - view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, -) -> MultiWindow> -where - State: 'static, - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: program::Renderer, - Executor: iced::Executor, -{ - use std::marker::PhantomData; - - impl Program - for Instance - where - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: program::Renderer, - Update: application::Update, - View: for<'a> self::View<'a, State, Message, Theme, Renderer>, - Executor: iced::Executor, - { - type State = State; - type Message = Message; - type Theme = Theme; - type Renderer = Renderer; - type Executor = Executor; - - fn update(&self, state: &mut Self::State, message: Self::Message) -> Task { - self.update.update(state, message).into() - } - - fn view<'a>( - &self, - state: &'a Self::State, - window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state, window).into() - } - } - - MultiWindow { - raw: Instance { - update, - view, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - _executor: PhantomData::, - }, - settings: Settings::default(), - window: None, - } - .title(title) -} - -/// The underlying definition and configuration of an iced daemon. -/// -/// You can use this API to create and run iced applications -/// step by step—without coupling your logic to a trait -/// or a specific type. -/// -/// You can create a [`MultiWindow`] with the [`daemon`] helper. -#[derive(Debug)] -pub struct MultiWindow { - raw: P, - settings: Settings, - window: Option, -} - -impl MultiWindow

{ - #[cfg(any(feature = "winit", feature = "wayland"))] - /// Runs the [`MultiWindow`]. - /// - /// The state of the [`MultiWindow`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with - pub fn run(self) -> Result - where - Self: 'static, - P::State: Default, - { - self.raw.run(self.settings, self.window) - } - - #[cfg(any(feature = "winit", feature = "wayland"))] - /// Runs the [`MultiWindow`] with a closure that creates the initial state. - pub fn run_with(self, initialize: I) -> Result - where - Self: 'static, - I: FnOnce() -> (P::State, Task) + 'static, - { - self.raw.run_with(self.settings, self.window, initialize) - } - - /// Sets the [`Settings`] that will be used to run the [`MultiWindow`]. - pub fn settings(self, settings: Settings) -> Self { - Self { settings, ..self } - } - - /// Sets the [`Title`] of the [`MultiWindow`]. - pub(crate) fn title( - self, - title: impl Title, - ) -> MultiWindow> { - MultiWindow { - raw: with_title(self.raw, move |state, window| title.title(state, window)), - settings: self.settings, - window: self.window, - } - } - - /// Sets the subscription logic of the [`MultiWindow`]. - pub fn subscription( - self, - f: impl Fn(&P::State) -> Subscription, - ) -> MultiWindow> { - MultiWindow { - raw: with_subscription(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the theme logic of the [`MultiWindow`]. - pub fn theme( - self, - f: impl Fn(&P::State, window::Id) -> P::Theme, - ) -> MultiWindow> { - MultiWindow { - raw: with_theme(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the style logic of the [`MultiWindow`]. - pub fn style( - self, - f: impl Fn(&P::State, &P::Theme) -> Appearance, - ) -> MultiWindow> { - MultiWindow { - raw: with_style(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the window settings of the [`MultiWindow`]. - pub fn window(self, window: window::Settings) -> Self { - Self { - raw: self.raw, - settings: self.settings, - window: Some(window), - } - } -} - -/// The title logic of some [`MultiWindow`]. -/// -/// This trait is implemented both for `&static str` and -/// any closure `Fn(&State, window::Id) -> String`. -/// -/// This trait allows the [`daemon`] builder to take any of them. -pub trait Title { - /// Produces the title of the [`MultiWindow`]. - fn title(&self, state: &State, window: window::Id) -> String; -} - -impl Title for &'static str { - fn title(&self, _state: &State, _window: window::Id) -> String { - (*self).to_string() - } -} - -impl Title for T -where - T: Fn(&State, window::Id) -> String, -{ - fn title(&self, state: &State, window: window::Id) -> String { - self(state, window) - } -} - -/// The view logic of some [`MultiWindow`]. -/// -/// This trait allows the [`daemon`] builder to take any closure that -/// returns any `Into>`. -pub trait View<'a, State, Message, Theme, Renderer> { - /// Produces the widget of the [`MultiWindow`]. - fn view( - &self, - state: &'a State, - window: window::Id, - ) -> impl Into>; -} - -impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T -where - T: Fn(&'a State, window::Id) -> Widget, - State: 'static, - Widget: Into>, -{ - fn view( - &self, - state: &'a State, - window: window::Id, - ) -> impl Into> { - self(state, window) - } -} diff --git a/src/applet/column.rs b/src/applet/column.rs index 8fa2fa9f..8b3c68e9 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -217,7 +217,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -233,25 +233,26 @@ where self.padding, self.spacing, self.align, - &self.children, + &mut self.children, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -261,17 +262,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let my_state = tree.state.downcast_mut::(); if let Some(hovered) = my_state.hovered { @@ -285,7 +286,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -302,7 +303,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -336,9 +337,9 @@ where ) && cursor.is_over(c_layout.bounds()) { my_state.hovered = Some(i); - return child.as_widget_mut().on_event( + return child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -350,9 +351,9 @@ where cursor_virtual = mouse::Cursor::Unavailable; } - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -360,8 +361,7 @@ where shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -436,11 +436,19 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0ab18817..ff376aab 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "applet-token")] pub mod token; -use crate::app::cosmic; +use crate::app::{BootData, BootDataInner, cosmic}; use crate::{ Application, Element, Renderer, app::iced_settings, @@ -18,17 +18,19 @@ use crate::{ self, autosize::{self, Autosize, autosize}, column::Column, - horizontal_space, layer_container, + layer_container, row::Row, - vertical_space, + space::horizontal, + space::vertical, }, }; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; use iced_core::{Padding, Shadow}; +use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; -use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; +use std::cell::RefCell; use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; @@ -386,6 +388,7 @@ impl Context { }, shadow: Shadow::default(), icon_color: Some(cosmic.background.on.into()), + snap: true, } }), ) @@ -567,30 +570,36 @@ pub fn run(flags: App::Flags) -> iced::Result { window_settings.decorations = false; window_settings.exit_on_close_request = true; window_settings.resizable = false; - window_settings.resize_border = 0; + // window_settings.resize_border = 0; // TODO make multi-window not mandatory - let mut app = super::app::multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // TODO still apply window settings? + // window_settings = window_settings.clone(); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if core.main_window.is_none() { - app = app.window(window_settings.clone()); - core.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .settings(iced_settings) - .run_with(move || cosmic::Cosmic::::init((core, flags))) + .run() } #[must_use] -pub fn style() -> iced_runtime::Appearance { +pub fn style() -> iced::theme::Style { let theme = crate::theme::THEME.lock().unwrap(); - iced_runtime::Appearance { + iced::theme::Style { background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), text_color: theme.cosmic().on_bg_color().into(), icon_color: theme.cosmic().on_bg_color().into(), diff --git a/src/applet/row.rs b/src/applet/row.rs index b5cf851f..2a770503 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -208,7 +208,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -222,25 +222,26 @@ where self.padding, self.spacing, self.align, - &self.children, + &mut self.children, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -250,17 +251,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let my_state = tree.state.downcast_mut::(); if let Some(hovered) = my_state.hovered { @@ -274,7 +275,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -291,7 +292,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -326,9 +327,9 @@ where ) && cursor.is_over(c_layout.bounds()) { my_state.hovered = Some(i); - return child.as_widget_mut().on_event( + return child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -340,9 +341,9 @@ where cursor_virtual = mouse::Cursor::Unavailable; } - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -350,8 +351,7 @@ where shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -426,11 +426,19 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 706c0301..82763303 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -14,16 +14,15 @@ use super::wayland_handler::wayland_handler; pub fn activation_token_subscription( id: I, ) -> iced::Subscription { - Subscription::run_with_id( - id, + Subscription::run_with(id, |_| { stream::channel(50, move |mut output| async move { let mut state = State::Ready; loop { state = start_listening(state, &mut output).await; } - }), - ) + }) + }) } pub enum State { diff --git a/src/command.rs b/src/command.rs index 00684e55..1d6f635c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -39,7 +39,7 @@ pub fn set_theme(theme: crate::Theme) -> iced::Task(id: window::Id) -> iced::Task> { - iced_runtime::window::change_mode(id, window::Mode::Windowed) + iced_runtime::window::set_mode(id, window::Mode::Windowed) } /// Toggles the windows' maximize state. diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index c8931dd4..99e2f9f0 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -16,75 +16,80 @@ use { #[cold] pub fn subscription() -> Subscription> { use iced_futures::futures::StreamExt; - iced_futures::Subscription::run_with_id( - TypeId::of::(), - iced::stream::channel(10, move |mut output| async move { - let mut single_instance: DbusActivation = DbusActivation::new(); - let mut rx = single_instance.rx(); - if let Ok(builder) = zbus::connection::Builder::session() { - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - if let Ok(conn) = builder.build().await { - // XXX Setup done this way seems to be more reliable. - // - // the docs for serve_at seem to imply it will replace the - // existing interface at the requested path, but it doesn't - // seem to work that way all the time. The docs for - // object_server().at() imply it won't replace the existing - // interface. - // - // request_name is used either way, with the builder or - // with the connection, but it must be done after the - // object server is setup. - if conn.object_server().at(path, single_instance).await != Ok(true) { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - if conn.request_name(App::APP_ID).await.is_err() { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } + iced_futures::Subscription::run_with(TypeId::of::(), |_| { + iced::stream::channel( + 10, + move |mut output: Sender>| async move { + let mut single_instance: DbusActivation = DbusActivation::new(); + let mut rx = single_instance.rx(); + if let Ok(builder) = zbus::connection::Builder::session() { + let path: String = format!("/{}", App::APP_ID.replace('.', "/")); + if let Ok(conn) = builder.build().await { + // XXX Setup done this way seems to be more reliable. + // + // the docs for serve_at seem to imply it will replace the + // existing interface at the requested path, but it doesn't + // seem to work that way all the time. The docs for + // object_server().at() imply it won't replace the existing + // interface. + // + // request_name is used either way, with the builder or + // with the connection, but it must be done after the + // object server is setup. + if conn.object_server().at(path, single_instance).await != Ok(true) { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + if conn.request_name(App::APP_ID).await.is_err() { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } - output - .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( - conn.clone(), - ))) - .await; + output + .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( + conn.clone(), + ))) + .await; - #[cfg(feature = "smol")] - let handle = { - std::thread::spawn(move || { - let conn_clone = _conn.clone(); + #[cfg(feature = "smol")] + let handle = { + std::thread::spawn(move || { + let conn_clone = _conn.clone(); - zbus::block_on(async move { - loop { - conn_clone.executor().tick().await; - } + zbus::block_on(async move { + loop { + conn_clone.executor().tick().await; + } + }) }) - }) - }; - while let Some(mut msg) = rx.next().await { - if let Some(token) = msg.activation_token.take() { - if let Err(err) = output - .send(crate::Action::Cosmic(crate::app::Action::Activate(token))) - .await + }; + while let Some(mut msg) = rx.next().await { + if let Some(token) = msg.activation_token.take() { + if let Err(err) = output + .send(crate::Action::Cosmic(crate::app::Action::Activate( + token, + ))) + .await + { + tracing::error!(?err, "Failed to send message"); + } + } + if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { tracing::error!(?err, "Failed to send message"); } } - if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { - tracing::error!(?err, "Failed to send message"); - } } + } else { + tracing::warn!("Failed to connect to dbus for single instance"); } - } else { - tracing::warn!("Failed to connect to dbus for single instance"); - } - loop { - iced::futures::pending!(); - } - }), - ) + loop { + iced::futures::pending!(); + } + }, + ) + }) } #[derive(Debug, Clone)] diff --git a/src/executor/multi.rs b/src/executor/multi.rs index 50aa111e..5536db54 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -26,4 +26,8 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } + + fn block_on(&self, future: impl Future) -> T { + self.0.block_on(future) + } } diff --git a/src/executor/single.rs b/src/executor/single.rs index aaa4f9f5..7c42ae84 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -30,4 +30,8 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } + + fn block_on(&self, future: impl Future) -> T { + self.0.block_on(future) + } } diff --git a/src/theme/portal.rs b/src/theme/portal.rs index f0c88c01..0154ff58 100644 --- a/src/theme/portal.rs +++ b/src/theme/portal.rs @@ -13,9 +13,8 @@ pub enum Desktop { #[cold] pub fn desktop_settings() -> iced_futures::Subscription { - iced_futures::Subscription::run_with_id( - std::any::TypeId::of::(), - stream::channel(10, |mut tx| { + iced_futures::Subscription::run(|| { + stream::channel(10, |mut tx: futures::channel::mpsc::Sender| { async move { let mut attempts = 0; loop { @@ -99,6 +98,6 @@ pub fn desktop_settings() -> iced_futures::Subscription { } } } - }), - ) + }) + }) } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 937ee388..4633477d 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -7,6 +7,7 @@ use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme}; use cosmic_theme::composite::over; use iced::{ overlay::menu, + theme::Base, widget::{ button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, pane_grid, pick_list, progress_bar, radio, rule, scrollable, @@ -15,7 +16,7 @@ use iced::{ }, }; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::{pane_grid::Highlight, text_editor, text_input}; +use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; @@ -36,13 +37,13 @@ pub mod application { } } - pub fn appearance(theme: &Theme) -> Appearance { + pub fn style(theme: &Theme) -> iced::theme::Style { let cosmic = theme.cosmic(); - Appearance { - icon_color: cosmic.bg_color().into(), + iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), } } } @@ -422,6 +423,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -436,6 +438,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -450,6 +453,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } } @@ -493,6 +497,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Container::List => { @@ -506,6 +511,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -552,6 +558,7 @@ impl iced_container::Catalog for Theme { .into(), ..Default::default() }, + snap: true, shadow: Shadow::default(), } } @@ -582,6 +589,7 @@ impl iced_container::Catalog for Theme { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), + snap: true, }, Container::Tooltip => iced_container::Style { @@ -593,6 +601,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Container::Card => { @@ -610,6 +619,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { icon_color: Some(Color::from(cosmic.primary.component.on)), @@ -622,6 +632,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { icon_color: Some(Color::from(cosmic.secondary.component.on)), @@ -634,6 +645,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, } } @@ -652,6 +664,7 @@ impl iced_container::Catalog for Theme { offset: Vector::new(0.0, 4.0), blur_radius: 16.0, }, + snap: true, }, } } @@ -791,6 +804,7 @@ impl menu::Catalog for Theme { }, selected_text_color: cosmic.accent_text_color().into(), selected_background: Background::Color(cosmic.background.component.hover.into()), + shadow: Default::default(), } } } @@ -830,7 +844,7 @@ impl pick_list::Catalog for Theme { background: Background::Color(cosmic.background.base.into()), ..appearance }, - pick_list::Status::Opened => appearance, + pick_list::Status::Opened { is_hovered: _ } => appearance, } } } @@ -920,6 +934,8 @@ impl toggler::Catalog for Theme { background_border_color: Color::TRANSPARENT, foreground_border_width: 0.0, foreground_border_color: Color::TRANSPARENT, + text_color: None, + padding_ratio: 0.0, }; match status { toggler::Status::Active { is_toggled } => active, @@ -942,9 +958,9 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled => { - active.background.a /= 2.; - active.foreground.a /= 2.; + toggler::Status::Disabled { is_toggled } => { + active.background = active.background.scale_alpha(0.5); + active.foreground = active.foreground.scale_alpha(0.5); active } } @@ -1086,21 +1102,21 @@ impl rule::Catalog for Theme { match class { Rule::Default => rule::Style { color: self.current_container().divider.into(), - width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, + snap: true, }, Rule::LightDivider => rule::Style { color: self.current_container().divider.into(), - width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Padded(8), + snap: true, }, Rule::HeavyDivider => rule::Style { color: self.current_container().divider.into(), - width: 4, radius: 2.0.into(), fill_mode: rule::FillMode::Full, + snap: true, }, Rule::Custom(f) => f(self), } @@ -1126,7 +1142,10 @@ impl scrollable::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { - scrollable::Status::Active => { + scrollable::Status::Active { + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, + } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); @@ -1139,7 +1158,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1157,7 +1176,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1169,6 +1188,13 @@ impl scrollable::Catalog for Theme { }, }, gap: None, + // TODO: what is auto scroll? + auto_scroll: AutoScroll { + background: Color::TRANSPARENT.into(), + border: Border::default(), + shadow: Shadow::default(), + icon: Color::TRANSPARENT.into(), + }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); @@ -1200,7 +1226,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1218,7 +1244,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1230,6 +1256,13 @@ impl scrollable::Catalog for Theme { }, }, gap: None, + // TODO: what is auto scroll? + auto_scroll: AutoScroll { + background: Color::TRANSPARENT.into(), + border: Border::default(), + shadow: Shadow::default(), + icon: Color::TRANSPARENT.into(), + }, }; if matches!(class, Scrollable::Permanent) { @@ -1400,7 +1433,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused => { + text_input::Status::Focused { is_hovered } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1477,7 +1510,8 @@ impl iced_widget::text_editor::Catalog for Theme { let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); - let icon = cosmic.background.on.into(); + let icon: Color = cosmic.background.on.into(); + // TODO do we need to add icon color back? match status { iced_widget::text_editor::Status::Active @@ -1489,23 +1523,23 @@ impl iced_widget::text_editor::Catalog for Theme { width: f32::from(cosmic.space_xxxs()), color: iced::Color::from(cosmic.bg_divider()), }, - icon, - placeholder, - value, - selection, - }, - iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style { - background: iced::Color::from(cosmic.bg_color()).into(), - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - color: iced::Color::from(cosmic.accent.base), - }, - icon, placeholder, value, selection, }, + iced_widget::text_editor::Status::Focused { is_hovered } => { + iced_widget::text_editor::Style { + background: iced::Color::from(cosmic.bg_color()).into(), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.accent.base), + }, + placeholder, + value, + selection, + } + } } } } @@ -1522,6 +1556,21 @@ impl iced_widget::markdown::Catalog for Theme { } } +impl iced_widget::table::Catalog for Theme { + type Class<'a> = iced_widget::table::StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|theme| iced_widget::table::Style { + separator_x: theme.current_container().divider.into(), + separator_y: theme.current_container().divider.into(), + }) + } + + fn style(&self, class: &Self::Class<'_>) -> iced_widget::table::Style { + class(self) + } +} + #[cfg(feature = "qr_code")] impl iced_widget::qr_code::Catalog for Theme { type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>; @@ -1539,3 +1588,50 @@ impl iced_widget::qr_code::Catalog for Theme { } impl combo_box::Catalog for Theme {} + +impl Base for Theme { + fn default(preference: iced::theme::Mode) -> Self { + match preference { + iced::theme::Mode::Light => Theme::light(), + iced::theme::Mode::Dark | iced::theme::Mode::None => Theme::dark(), + } + } + + fn mode(&self) -> iced::theme::Mode { + if self.theme_type.is_dark() { + iced::theme::Mode::Dark + } else { + iced::theme::Mode::Light + } + } + + fn base(&self) -> iced::theme::Style { + iced::theme::Style { + background_color: self.cosmic().bg_color().into(), + text_color: self.cosmic().on_bg_color().into(), + icon_color: self.cosmic().on_bg_color().into(), + } + } + + fn palette(&self) -> Option { + Some(iced::theme::Palette { + primary: self.cosmic().accent.base.into(), + success: self.cosmic().success.base.into(), + warning: self.cosmic().warning.base.into(), + danger: self.cosmic().destructive.base.into(), + background: iced::Color::from(self.cosmic().bg_color()), + text: iced::Color::from(self.cosmic().on_bg_color()), + }) + } + + fn name(&self) -> &str { + match &self.theme_type { + crate::theme::ThemeType::Dark => "Cosmic Dark Theme", + crate::theme::ThemeType::Light => "Cosmic Light Theme", + crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme", + crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme", + crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", + crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, + } + } +} diff --git a/src/widget/about.rs b/src/widget/about.rs index 384aee4a..ba88e03a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,7 +1,7 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, horizontal_space}, + widget::{self, space}, }; #[derive(Debug, Default, Clone, derive_setters::Setters)] @@ -99,7 +99,7 @@ pub fn about<'a, Message: Clone + 'static>( let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { widget::row() .push(widget::text(name)) - .push(horizontal_space()) + .push(space::horizontal()) .push_maybe( (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), ) diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index e66c14d0..577bea95 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -2,7 +2,7 @@ use iced::Size; use iced::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout; use iced_core::mouse; use iced_core::overlay; @@ -172,7 +172,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,7 +186,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -195,18 +195,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -254,11 +254,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } #[cfg(feature = "a11y")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 172d505f..6a1e6060 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -5,7 +5,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree}; +use iced_core::widget::{Id, Operation, Tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; @@ -115,7 +115,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -131,22 +131,23 @@ where } let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, &my_limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -158,17 +159,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { #[cfg(feature = "wayland")] if matches!( event, @@ -179,9 +180,9 @@ where let bounds = layout.bounds().size(); clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.)); } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout .children() .next() @@ -238,8 +239,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -250,6 +252,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 87233330..54e29786 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -318,7 +318,7 @@ impl<'a, Message: 'a + Clone> Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -331,21 +331,22 @@ impl<'a, Message: 'a + Clone> Widget self.padding, |renderer, limits| { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -357,20 +358,19 @@ impl<'a, Message: 'a + Clone> Widget ); }); let state = tree.state.downcast_mut::(); - operation.focusable(state, Some(&self.id)); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { if let Variant::Image { on_remove: Some(on_remove), .. @@ -383,7 +383,8 @@ impl<'a, Message: 'a + Clone> Widget if let Some(position) = cursor.position() { if removal_bounds(layout.bounds(), 4.0).contains(position) { shell.publish(on_remove.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -391,10 +392,9 @@ impl<'a, Message: 'a + Clone> Widget _ => (), } } - - if self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout .children() .next() @@ -405,9 +405,9 @@ impl<'a, Message: 'a + Clone> Widget clipboard, shell, viewport, - ) == event::Status::Captured - { - return event::Status::Captured; + ); + if shell.is_event_captured() { + return; } update( @@ -541,6 +541,7 @@ impl<'a, Message: 'a + Clone> Widget ..Default::default() }, shadow: Shadow::default(), + snap: true, }, selection_background, ); @@ -554,7 +555,7 @@ impl<'a, Message: 'a + Clone> Widget y: bounds.y + (bounds.height - 18.0 - styling.border_width), }; if bounds.intersects(viewport) { - iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds); + iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); } } @@ -570,6 +571,7 @@ impl<'a, Message: 'a + Clone> Widget radius: c_rad.radius_m.into(), ..Default::default() }, + snap: true, }, selection_background, ); @@ -583,6 +585,12 @@ impl<'a, Message: 'a + Clone> Widget x: bounds.x + 4.0, y: bounds.y + 4.0, }, + Rectangle { + width: 16.0, + height: 16.0, + x: bounds.x + 4.0, + y: bounds.y + 4.0, + }, ); } } @@ -609,8 +617,9 @@ impl<'a, Message: 'a + Clone> Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -624,6 +633,7 @@ impl<'a, Message: 'a + Clone> Widget .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } @@ -638,7 +648,7 @@ impl<'a, Message: 'a + Clone> Widget ) -> iced_accessibility::A11yTree { use iced_accessibility::{ A11yNode, A11yTree, - accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role}, + accesskit::{Action, Node, NodeId, Rect, Role}, }; // TODO why is state None sometimes? if matches!(state.state, iced_core::widget::tree::State::None) { @@ -658,12 +668,12 @@ impl<'a, Message: 'a + Clone> Widget let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64); let is_hovered = state.state.downcast_ref::().is_hovered; - let mut node = NodeBuilder::new(Role::Button); + let mut node = Node::new(Role::Button); node.add_action(Action::Focus); - node.add_action(Action::Default); + node.add_action(Action::Click); node.set_bounds(bounds); if let Some(name) = self.name.as_ref() { - node.set_name(name.clone()); + node.set_label(name.clone()); } match self.description.as_ref() { Some(iced_accessibility::Description::Id(id)) => { @@ -682,10 +692,10 @@ impl<'a, Message: 'a + Clone> Widget if self.on_press.is_none() { node.set_disabled(); } - if is_hovered { - node.set_hovered(); - } - node.set_default_action_verb(DefaultActionVerb::Click); + // TODO hover + // if is_hovered { + // node.set_hovered(); + // } if let Some(child_tree) = child_tree.map(|child_tree| { self.content.as_widget().a11y_nodes( @@ -761,14 +771,14 @@ impl State { #[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)] pub fn update<'a, Message: Clone>( _id: Id, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>, on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -787,7 +797,8 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -806,7 +817,8 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } } else if on_press_down.is_some() { let state = state(); @@ -816,7 +828,7 @@ pub fn update<'a, Message: Clone>( #[cfg(feature = "a11y")] Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); - if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Default) + if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) .then_some(on_press) .flatten() { @@ -825,17 +837,19 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press { let state = state(); - if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { + if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) { state.is_pressed = true; let msg = (on_press)(layout.virtual_offset(), layout.bounds()); shell.publish(msg); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -846,8 +860,6 @@ pub fn update<'a, Message: Clone>( } _ => {} } - - event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -879,6 +891,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -900,6 +913,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -915,6 +929,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background, ); @@ -930,6 +945,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, overlay, ); @@ -953,6 +969,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index ea10fddb..7c09d39c 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -213,7 +213,9 @@ where let content_list = column::with_children([ row::with_children([ column().push(date).push(day).into(), - crate::widget::Space::with_width(Length::Fill).into(), + crate::widget::space::horizontal() + .width(Length::Fill) + .into(), month_controls.into(), ]) .align_y(Vertical::Center) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 40a4a940..d484bb62 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -26,7 +26,10 @@ use iced_core::{ }; use iced_widget::slider::HandleShape; -use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space}; +use iced_widget::{ + Row, canvas, column, row, scrollable, + space::{horizontal, vertical}, +}; use palette::{FromColor, RgbHue}; use super::divider::horizontal; @@ -334,7 +337,7 @@ where .width(self.width), // canvas with gradient for the current color // still needs the canvas and the handle to be drawn on it - container(vertical_space().height(self.height)) + container(vertical().height(self.height)) .width(self.width) .height(self.height), slider( @@ -548,13 +551,13 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.inner - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -657,6 +660,7 @@ where radius: (1.0 + handle_radius).into(), }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -674,6 +678,7 @@ where radius: handle_radius.into(), }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -684,26 +689,31 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.inner - .as_widget_mut() - .overlay(&mut state.children[0], layout, renderer, translation) + self.inner.as_widget_mut().overlay( + &mut state.children[0], + layout, + renderer, + viewport, + translation, + ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { // if the pointer is performing a drag, intercept pointer motion and button events // else check if event is handled by child elements // if the event is not handled by a child element, check if it is over the canvas when pressing a button @@ -732,24 +742,26 @@ where shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished)); state.dragging = false; } - _ => return event::Status::Ignored, + _ => return, }; - return event::Status::Captured; + shell.capture_event(); + return; } let column_tree = &mut tree.children[0]; - if self.inner.as_widget_mut().on_event( + self.inner.as_widget_mut().update( column_tree, - event.clone(), + &event, column_layout, cursor, renderer, clipboard, shell, viewport, - ) == event::Status::Captured - { - return event::Status::Captured; + ); + if shell.is_event_captured() { + shell.capture_event(); + return; } match event { @@ -764,12 +776,10 @@ where state.dragging = true; let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } - _ => event::Status::Ignored, + _ => {} } } @@ -812,12 +822,12 @@ pub fn color_button<'a, Message: Clone + 'static>( let spacing = THEME.lock().unwrap().cosmic().spacing; button::custom(if color.is_some() { - Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s)))) + Element::from(vertical().height(Length::Fixed(f32::from(spacing.space_s)))) } else { Element::from(column![ - vertical_space().height(Length::FillPortion(6)), + vertical().height(Length::FillPortion(6)), row![ - horizontal_space().width(Length::FillPortion(6)), + horizontal().width(Length::FillPortion(6)), Icon::from( icon::from_name("list-add-symbolic") .prefer_svg(true) @@ -827,11 +837,11 @@ pub fn color_button<'a, Message: Clone + 'static>( .width(icon_portion) .height(Length::Fill) .content_fit(iced_core::ContentFit::Contain), - horizontal_space().width(Length::FillPortion(6)), + horizontal().width(Length::FillPortion(6)), ] .height(icon_portion) .width(Length::Fill), - vertical_space().height(Length::FillPortion(6)), + vertical().height(Length::FillPortion(6)), ]) }) .width(Length::Fixed(f32::from(spacing.space_s))) diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 4f72e113..eef9183b 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -7,7 +7,7 @@ use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Operation}; use iced::advanced::{Clipboard, Shell}; use iced::advanced::{overlay, renderer}; -use iced::{Event, Point, Rectangle, Size, event, mouse}; +use iced::{Event, Point, Size, mouse}; use iced_core::Renderer; pub(super) struct Overlay<'a, 'b, Message> { @@ -29,7 +29,7 @@ where let node = self .content - .as_widget() + .as_widget_mut() .layout(self.tree, renderer, &limits); let node_size = node.size(); @@ -47,16 +47,16 @@ where }) } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( self.tree, event, layout, @@ -104,9 +104,10 @@ where &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { + // TODO how to handle viewport here? + let viewport = &layout.bounds(); self.content .as_widget() .mouse_interaction(self.tree, layout, cursor, viewport, renderer) @@ -114,11 +115,17 @@ where fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &crate::Renderer, ) -> Option> { - self.content - .as_widget_mut() - .overlay(self.tree, layout, renderer, iced::Vector::default()) + let viewport = &layout.bounds(); + + self.content.as_widget_mut().overlay( + self.tree, + layout, + renderer, + viewport, + iced::Vector::default(), + ) } } diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 5366832f..e7ca5dab 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -7,7 +7,7 @@ use crate::{Apply, Element, Renderer, Theme, fl}; use std::borrow::Cow; use iced_core::Alignment; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, @@ -65,7 +65,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { } else { let title = title .map(|title| text::title4(title).width(Length::Fill).apply(Element::from)) - .unwrap_or_else(|| widget::horizontal_space().apply(Element::from)); + .unwrap_or_else(|| widget::space::horizontal().apply(Element::from)); (title, None) }; @@ -196,40 +196,40 @@ impl Widget for ContextDrawer<' } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -282,8 +282,9 @@ impl Widget for ContextDrawer<' fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, _renderer: &Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let bounds = layout.bounds(); diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index d9dc529a..008660a7 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -270,13 +270,13 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &iced_core::layout::Limits, ) -> iced_core::layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -302,29 +302,29 @@ impl Widget } fn operate( - &self, + &mut self, tree: &mut Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: iced::Event, + event: &iced::Event, layout: iced_core::Layout<'_>, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced::Rectangle, - ) -> iced_core::event::Status { + ) { let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); @@ -384,7 +384,7 @@ impl Widget match event { Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(id); + state.fingers_pressed.insert(*id); } Event::Touch(touch::Event::FingerLifted { id, .. }) => { @@ -410,7 +410,8 @@ impl Widget self.create_popup(layout, cursor, renderer, shell, viewport, state); } - return event::Status::Captured; + shell.capture_event(); + return; } else if !was_open && right_button_released(&event) || (touch_lifted(&event)) || left_button_released(&event) @@ -440,7 +441,7 @@ impl Widget }); } } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -457,6 +458,7 @@ impl Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, + viewport: &iced::Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index ba5b55e2..7d084626 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -123,7 +123,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(body) = dialog.body { if should_space { content_col = content_col - .push(widget::vertical_space().height(Length::Fixed(space_xxs.into()))); + .push(widget::space::vertical().height(Length::Fixed(space_xxs.into()))); } content_col = content_col.push( widget::container(widget::scrollable(widget::text::body(body))).max_height(300.), @@ -133,7 +133,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes for control in dialog.controls { if should_space { content_col = content_col - .push(widget::vertical_space().height(Length::Fixed(space_s.into()))); + .push(widget::space::vertical().height(Length::Fixed(space_s.into()))); } content_col = content_col.push(control); should_space = true; @@ -149,7 +149,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(button) = dialog.tertiary_action { button_row = button_row.push(button); } - button_row = button_row.push(widget::horizontal_space()); + button_row = button_row.push(widget::space::horizontal()); if let Some(button) = dialog.secondary_action { button_row = button_row.push(button); } diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 7225e917..9faa2605 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -303,43 +303,43 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.container - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: layout::Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let s = self.container.as_widget_mut().on_event( + ) { + let s = self.container.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout, cursor, renderer, @@ -347,8 +347,8 @@ impl Widget shell, viewport, ); - if matches!(s, event::Status::Captured) { - return event::Status::Captured; + if shell.is_event_captured() { + return; } let state = tree.state.downcast_mut::>(); @@ -367,23 +367,23 @@ impl Widget OfferEvent::Enter { x, y, mime_types, .. }, - )) if id == Some(my_id) => { + )) if *id == Some(my_id) => { if !self.mime_matches(&mime_types) { log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", self.mime_types ); - return event::Status::Ignored; + return; } log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}" ); if let Some(msg) = state.on_enter( - x, - y, - mime_types, + *x, + *y, + mime_types.clone(), self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), ) { @@ -391,13 +391,13 @@ impl Widget } if self.forward_drag_as_cursor { #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into()); + let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); let event = Event::Mouse(mouse::Event::CursorMoved { position: drag_cursor.position().unwrap(), }); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -406,7 +406,8 @@ impl Widget viewport, ); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -423,9 +424,9 @@ impl Widget if self.forward_drag_as_cursor { let drag_cursor = mouse::Cursor::Unavailable; let event = Event::Mouse(mouse::Event::CursorLeft); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -434,16 +435,16 @@ impl Widget viewport, ); } - return event::Status::Ignored; + return; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => { + Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer motion id={my_id:?} coords=({x},{y})" ); if let Some(msg) = state.on_motion( - x, - y, + *x, + *y, self.on_motion.as_ref().map(std::convert::AsRef::as_ref), self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), @@ -453,13 +454,13 @@ impl Widget if self.forward_drag_as_cursor { #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into()); + let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); let event = Event::Mouse(mouse::Event::CursorMoved { position: drag_cursor.position().unwrap(), }); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -468,7 +469,8 @@ impl Widget viewport, ); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -481,9 +483,9 @@ impl Widget { shell.publish(msg); } - return event::Status::Ignored; + return; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { + Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer drop id={my_id:?}" @@ -493,27 +495,29 @@ impl Widget { shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) - if id == Some(my_id) => + if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer selected-action id={my_id:?} action={action:?}" ); if let Some(msg) = state.on_action_selected( - action, + *action, self.on_action_selected .as_ref() .map(std::convert::AsRef::as_ref), ) { shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) - if id == Some(my_id) => + if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, @@ -531,21 +535,28 @@ impl Widget } if let (Some(msg), ret) = state.on_data_received( - mime_type, - data, + mime_type.clone(), + data.clone(), self.on_data_received .as_ref() .map(std::convert::AsRef::as_ref), self.on_finish.as_ref().map(std::convert::AsRef::as_ref), ) { shell.publish(msg); - return ret; + if ret == event::Status::Captured { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer data id={my_id:?} captured" + ); + shell.capture_event(); + } + return; } - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} } - event::Status::Ignored } fn mouse_interaction( @@ -589,13 +600,18 @@ impl Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'_>, + layout: layout::Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, translation) + self.container.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index f21f9670..c8627482 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,6 +1,6 @@ use std::any::Any; -use iced_core::window; +use iced_core::{widget::Operation, window}; use crate::{ Element, @@ -176,7 +176,7 @@ impl(); let node = self .container - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); state.cached_bounds = node.bounds(); node } fn operate( - &self, + &mut self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id)); - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.container - .as_widget() - .operate(&mut tree.children[0], layout, renderer, operation) + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.container.as_widget_mut().operate( + tree, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: layout::Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let ret = self.container.as_widget_mut().on_event( + ) { + let ret = self.container.as_widget_mut().update( &mut tree.children[0], - event.clone(), + &event, layout, cursor, renderer, @@ -238,14 +245,16 @@ impl { state.left_pressed_position = None; - return event::Status::Captured; + shell.capture_event(); + return; } mouse::Event::CursorMoved { .. } => { if let Some(position) = cursor.position() { @@ -277,7 +286,8 @@ impl return ret, @@ -288,7 +298,8 @@ impl( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'_>, + layout: layout::Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, translation) + self.container.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 3fd099b3..0c96c1c6 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -213,7 +213,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { } } - fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + fn _layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { let space_below = bounds.height - (self.position.y + self.target_height); let space_above = self.position.y; @@ -242,19 +242,19 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { }) } - fn _on_event( + fn _update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); self.state.with_data_mut(|tree| { - self.container.on_event( + self.container.update( tree, event, layout, cursor, renderer, clipboard, shell, &bounds, ) }) @@ -293,6 +293,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { radius: appearance.border_radius, }, shadow: Shadow::default(), + snap: true, }, appearance.background, ); @@ -311,26 +312,25 @@ impl<'a, Message: Clone + 'a> iced_core::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self._on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self._update(event, layout, cursor, renderer, clipboard, shell) } fn mouse_interaction( &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { - self._mouse_interaction(layout, cursor, viewport, renderer) + self._mouse_interaction(layout, cursor, &layout.bounds(), renderer) } fn draw( @@ -353,7 +353,7 @@ impl<'a, Message: Clone + 'a> crate::widget::Widget crate::widget::Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { - self._on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self._update(event, layout, cursor, renderer, clipboard, shell) } fn draw( @@ -435,7 +435,7 @@ where } fn layout( - &self, + &mut self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -452,7 +452,7 @@ where let size = { let intrinsic = Size::new( 0.0, - (f32::from(text_line_height) + self.padding.vertical()) * self.options.len() as f32, + (f32::from(text_line_height) + self.padding.y()) * self.options.len() as f32, ); limits.resolve(Length::Fill, Length::Shrink, intrinsic) @@ -461,17 +461,17 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, _state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let hovered_guard = self.hovered_option.lock().unwrap(); @@ -481,7 +481,8 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -493,7 +494,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + + self.padding.y(); let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); @@ -515,7 +516,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + + self.padding.y(); let mut hovered_guard = self.hovered_option.lock().unwrap(); *hovered_guard = Some((cursor_position.y / option_height) as usize); @@ -525,14 +526,13 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - return event::Status::Captured; + shell.capture_event(); + return; } } } _ => {} } - - event::Status::Ignored } fn mouse_interaction( @@ -568,8 +568,8 @@ where let text_size = self .text_size .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + let option_height = + f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.y(); let offset = viewport.y - bounds.y; let start = (offset / option_height) as usize; @@ -605,6 +605,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.selected_background, ); @@ -614,16 +615,13 @@ where .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }, - ); + let bounds = Rectangle { + x: item_x + item_width - 16.0 - 8.0, + y: bounds.y + (bounds.height / 2.0 - 8.0), + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); (appearance.selected_text_color, crate::font::semibold()) } else if *hovered_guard == Some(i) { @@ -642,6 +640,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.hovered_background, ); @@ -678,8 +677,8 @@ where size: Pixels(text_size), line_height: self.text_line_height, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index fa4184c4..b2d3fbed 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -56,12 +56,12 @@ pub fn popup_dropdown< dropdown } -/// Produces a [`Task`] that closes the [`Dropdown`]. -pub fn close(id: Id) -> iced_runtime::Task { - iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) -} +// /// Produces a [`Task`] that closes the [`Dropdown`]. +// pub fn close(id: Id) -> iced_runtime::Task { +// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) +// } -/// Produces a [`Task`] that opens the [`Dropdown`]. -pub fn open(id: Id) -> iced_runtime::Task { - iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) -} +// /// Produces a [`Task`] that opens the [`Dropdown`]. +// pub fn open(id: Id) -> iced_runtime::Task { +// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) +// } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 39e89ee2..0a761097 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -209,18 +209,18 @@ impl iced_core::Overlay for Ove }) } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); - self.container.on_event( + self.container.update( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) } @@ -229,11 +229,10 @@ impl iced_core::Overlay for Ove &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { self.container - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + .mouse_interaction(self.state, layout, cursor, &layout.bounds(), renderer) } fn draw( @@ -256,6 +255,7 @@ impl iced_core::Overlay for Ove radius: appearance.border_radius, }, shadow: Shadow::default(), + snap: true, }, appearance.background, ); @@ -287,7 +287,7 @@ where } fn layout( - &self, + &mut self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -309,7 +309,7 @@ where ) }); - let vertical_padding = self.padding.vertical(); + let vertical_padding = self.padding.y(); let text_line_height = f32::from(text_line_height); let size = { @@ -328,17 +328,17 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, _state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); match event { @@ -346,7 +346,8 @@ where if cursor.is_over(bounds) { if let Some(item) = self.hovered_option.as_ref() { shell.publish((self.on_selected)(item.clone())); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -361,7 +362,7 @@ where let heights = self .options - .element_heights(self.padding.vertical(), text_line_height); + .element_heights(self.padding.y(), text_line_height); let mut current_offset = 0.0; @@ -408,7 +409,7 @@ where let heights = self .options - .element_heights(self.padding.vertical(), text_line_height); + .element_heights(self.padding.y(), text_line_height); let mut current_offset = 0.0; @@ -446,8 +447,6 @@ where } _ => {} } - - event::Status::Ignored } fn mouse_interaction( @@ -490,7 +489,7 @@ where let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))); let visible_options = self.options.visible_options( - self.padding.vertical(), + self.padding.y(), text_line_height, offset, viewport.height, @@ -528,24 +527,23 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.selected_background, ); + let svg_bounds = Rectangle { + x: item_x + item_width - 16.0 - 8.0, + y: bounds.y + (bounds.height / 2.0 - 8.0), + width: 16.0, + height: 16.0, + }; + let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone()) .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }, - ); + svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); (appearance.selected_text_color, crate::font::semibold()) } else if self.hovered_option.as_ref() == Some(item) { @@ -566,6 +564,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.hovered_background, ); @@ -590,8 +589,8 @@ where size: iced::Pixels(text_size), line_height: self.text_line_height, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -611,7 +610,7 @@ where }) .move_to(Point { x: bounds.x, - y: bounds.y + (self.padding.vertical() / 2.0) - 4.0, + y: bounds.y + (self.padding.y() / 2.0) - 4.0, }); Widget::::draw( @@ -640,8 +639,8 @@ where size: iced::Pixels(text_size), line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), font: crate::font::default(), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Center, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 43a0836f..a46c6dcc 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -78,7 +78,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -116,17 +116,17 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { update( &event, layout, @@ -183,8 +183,9 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); @@ -275,8 +276,8 @@ pub fn layout( size: iced::Pixels(text_size), line_height: text_line_height, font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -314,7 +315,7 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a on_selected: &dyn Fn(Item) -> Message, selections: &super::Model, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -325,14 +326,12 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a // bounds or on the drop-down, either way we close the overlay. state.is_open = false; - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { state.is_open = true; state.hovered_option = selections.selected.clone(); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -348,19 +347,15 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a shell.publish((on_selected)(option.1.clone())); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); state.keyboard_modifiers = *modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } } @@ -420,8 +415,8 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static size: iced::Pixels(text_size), line_height, font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -430,7 +425,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static }; let mut desc_count = 0; - padding.horizontal().mul_add( + padding.x().mul_add( 2.0, selections .elements() @@ -517,22 +512,20 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( bounds, border: style.border, shadow: Shadow::default(), + snap: true, }, style.background, ); if let Some(handle) = state.icon.as_ref() { let svg_handle = iced_core::Svg::new(handle.clone()).color(style.text_color); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }, - ); + let svg_bounds = Rectangle { + x: bounds.x + bounds.width - gap - 16.0, + y: bounds.center_y() - 8.0, + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); } if let Some(content) = selected.map(AsRef::as_ref) { @@ -541,7 +534,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( let bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.horizontal(), + width: bounds.width - padding.x(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -553,8 +546,8 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( line_height: text_line_height, font, bounds: bounds.size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs index 8cea4566..1a4e1a9f 100644 --- a/src/widget/dropdown/operation.rs +++ b/src/widget/dropdown/operation.rs @@ -11,62 +11,62 @@ pub trait Dropdown { fn open(&mut self); } -/// Produces a [`Task`] that closes a [`Dropdown`] popup. -pub fn close(id: Id) -> impl Operation { - struct Close(Id); +// /// Produces a [`Task`] that closes a [`Dropdown`] popup. +// pub fn close(id: Id) -> impl Operation { +// struct Close(Id); - impl Operation for Close { - fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { - if id.map_or(true, |id| id != &self.0) { - return; - } +// impl Operation for Close { +// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { +// if id.map_or(true, |id| id != &self.0) { +// return; +// } - let Some(state) = state.downcast_mut::() else { - return; - }; +// let Some(state) = state.downcast_mut::() else { +// return; +// }; - state.close(); - } +// state.close(); +// } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } +// fn container( +// &mut self, +// _id: Option<&Id>, +// _bounds: Rectangle, +// operate_on_children: &mut dyn FnMut(&mut dyn Operation), +// ) { +// operate_on_children(self) +// } +// } - Close(id) -} +// Close(id) +// } -/// Produces a [`Task`] that opens a [`Dropdown`] popup. -pub fn open(id: Id) -> impl Operation { - struct Open(Id); +// /// Produces a [`Task`] that opens a [`Dropdown`] popup. +// pub fn open(id: Id) -> impl Operation { +// struct Open(Id); - impl Operation for Open { - fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { - if id.map_or(true, |id| id != &self.0) { - return; - } +// impl Operation for Open { +// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { +// if id.map_or(true, |id| id != &self.0) { +// return; +// } - let Some(state) = state.downcast_mut::() else { - return; - }; +// let Some(state) = state.downcast_mut::() else { +// return; +// }; - state.open(); - } +// state.open(); +// } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } +// fn container( +// &mut self, +// _id: Option<&Id>, +// _bounds: Rectangle, +// operate_on_children: &mut dyn FnMut(&mut dyn Operation), +// ) { +// operate_on_children(self) +// } +// } - Open(id) -} +// Open(id) +// } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 67101d26..b6244c07 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -203,13 +203,13 @@ where state.hashes[i] = text_hash; state.selections[i].update(Text { content: selection.as_ref(), - bounds: Size::INFINITY, + bounds: Size::INFINITE, // TODO use the renderer default size size: iced::Pixels(self.text_size.unwrap_or(14.0)), line_height: self.text_line_height, font: self.font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -227,7 +227,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -252,17 +252,17 @@ where ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { update::( &event, layout, @@ -327,21 +327,23 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation, ) { - let state = tree.state.downcast_mut::(); - operation.custom(state, self.id.as_ref()); + // TODO: double check operation handling + // let state = tree.state.downcast_mut::(); + // operation.custom(state, self.id.as_ref()); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "winit", feature = "wayland"))] @@ -469,24 +471,38 @@ pub fn layout( let max_width = match width { Length::Shrink => { let measure = move |(label, paragraph): (_, Option<&mut crate::Plain>)| -> f32 { - let text = Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height: text_line_height, - font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }; let paragraph = match paragraph { Some(p) => { + let text = Text { + content: label, + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(crate::font::default), + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), + }; p.update(text); p } - None => &mut crate::Plain::new(text), + None => { + let text = Text { + content: label.to_string(), + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(crate::font::default), + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), + }; + &mut crate::Plain::new(text) + } }; paragraph.min_width().round() }; @@ -544,7 +560,7 @@ pub fn update< text_size: Option, font: Option, selected_option: Option, -) -> event::Status { +) { let state = state(); let open = |shell: &mut Shell<'_, Message>, @@ -575,7 +591,7 @@ pub fn update< let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { selection_paragraph.min_width().round() }; - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let selections_width = selections .iter() @@ -669,12 +685,10 @@ pub fn update< if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { open(shell, state, on_selected); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -689,17 +703,13 @@ pub fn update< shell.publish((on_selected)(next_index)); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = *modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } } @@ -746,7 +756,7 @@ where .zip(state.selections.iter()) .map(|(label, selection)| measure(label.as_ref(), selection.raw())) .fold(0.0, |next, current| current.max(next)); - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let width = selections_width + gap + pad_width + icon_width; let is_open = state.is_open.clone(); @@ -822,7 +832,7 @@ where selection_paragraph.min_width().round() }; - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; @@ -883,23 +893,20 @@ pub fn draw<'a, S>( bounds, border: style.border, shadow: Shadow::default(), + snap: true, }, style.background, ); if let Some(handle) = state.icon.clone() { let svg_handle = svg::Svg::new(handle).color(style.text_color); - - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }, - ); + let bounds = Rectangle { + x: bounds.x + bounds.width - gap - 16.0, + y: bounds.center_y() - 8.0, + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); } if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { @@ -908,7 +915,7 @@ pub fn draw<'a, S>( let mut bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.horizontal(), + width: bounds.width - padding.x(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -932,8 +939,8 @@ pub fn draw<'a, S>( line_height: text_line_height, font, bounds: bounds.size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index 744b607d..ae0c28d6 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -15,7 +15,7 @@ use taffy::{AlignContent, TaffyTree}; pub fn resolve( renderer: &Renderer, limits: &Limits, - items: &[Element<'_, Message>], + items: &mut [Element<'_, Message>], padding: Padding, column_spacing: f32, row_spacing: f32, @@ -61,8 +61,8 @@ pub fn resolve( ..taffy::Style::default() }; - for (child, tree) in items.iter().zip(tree.iter_mut()) { - let child_widget = child.as_widget(); + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + let child_widget = child.as_widget_mut(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -138,7 +138,7 @@ pub fn resolve( leafs .into_iter() - .zip(items.iter()) + .zip(items.iter_mut()) .zip(nodes.iter_mut()) .zip(tree) .for_each(|(((leaf, child), node), tree)| { @@ -146,7 +146,7 @@ pub fn resolve( return; }; - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 264201c1..f7b90f66 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -100,7 +100,7 @@ impl Widget for FlexR } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -114,7 +114,7 @@ impl Widget for FlexR super::layout::resolve( renderer, &limits, - &self.children, + &mut self.children, self.padding, f32::from(self.column_spacing), f32::from(self.row_spacing), @@ -127,19 +127,19 @@ impl Widget for FlexR } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -149,25 +149,25 @@ impl Widget for FlexR }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((child, state), c_layout)| { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor, renderer, @@ -175,8 +175,7 @@ impl Widget for FlexR shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -235,11 +234,19 @@ impl Widget for FlexR fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/widget/frames.rs b/src/widget/frames.rs index 1c379ac1..056a55ba 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -8,6 +8,8 @@ use std::path::Path; use std::time::{Duration, Instant}; use ::image as image_rs; +use iced::Task; +use iced::mouse; use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; @@ -15,7 +17,6 @@ use iced_core::{ Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, event, layout, renderer, window, }; -use iced_runtime::Command; use iced_widget::image::{self, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; @@ -27,7 +28,7 @@ use iced_futures::futures::{AsyncRead, AsyncReadExt}; #[cfg(feature = "tokio")] use tokio::io::{AsyncRead, AsyncReadExt}; -use super::icon::load_icon; +use crate::widget::icon; #[must_use] /// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`] @@ -74,13 +75,13 @@ impl Frames { size: u16, theme: Option<&str>, default_fallbacks: bool, - ) -> Command> { + ) -> Task> { let mut name_path_buffer = None; - if let Some(path) = load_icon(name, size, theme) { + if let Some(path) = icon::Named::new(name).size(size).path() { name_path_buffer = Some(path); } else if default_fallbacks { for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - if let Some(path) = load_icon(name, size, theme) { + if let Some(path) = icon::Named::new(name).size(size).path() { name_path_buffer = Some(path); break; } @@ -90,14 +91,14 @@ impl Frames { if let Some(name_path_buffer) = name_path_buffer { Self::load_from_path(name_path_buffer) } else { - Command::perform(async { Err(Error::Missing) }, std::convert::identity) + Task::perform(async { Err(Error::Missing) }, std::convert::identity) } } /// Load [`Frames`] from the supplied path - pub fn load_from_path(path: impl AsRef) -> Command> { + pub fn load_from_path(path: impl AsRef) -> Task> { #[inline(never)] - fn inner(path: &Path) -> Command> { + fn inner(path: &Path) -> Task> { #[cfg(feature = "tokio")] use tokio::fs::File; #[cfg(feature = "tokio")] @@ -108,7 +109,7 @@ impl Frames { #[cfg(not(feature = "tokio"))] use iced_futures::futures::io::BufReader; - let path = path.as_ref().to_path_buf(); + let path = path.to_path_buf(); let f = async move { let image_type = match &path.extension() { @@ -119,10 +120,10 @@ impl Frames { }; let reader = BufReader::new(File::open(path).await?); - Self::from_reader(reader, image_type).await + Frames::from_reader(reader, image_type).await }; - Command::perform(f, std::convert::identity) + Task::perform(f, std::convert::identity) } inner(path.as_ref()) @@ -168,9 +169,9 @@ impl Frames { let total_bytes = frames .iter() .map(|f| match f.handle.data() { - iced_core::image::Data::Path(_) => 0, - iced_core::image::Data::Bytes(b) => b.len(), - iced_core::image::Data::Rgba { pixels, .. } => pixels.len(), + iced_core::image::Handle::Path(..) => 0, + iced_core::image::Handle::Bytes(_, b) => b.len(), + iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -195,7 +196,7 @@ impl From for Frame { let delay = frame.delay().into(); - let handle = image::Handle::from_pixels(width, height, frame.into_buffer().into_vec()); + let handle = image::Handle::from_rgba(width, height, frame.into_buffer().into_vec()); Self { delay, handle } } @@ -278,12 +279,8 @@ impl<'a, Message, Renderer> Widget for Animated where Renderer: ImageRenderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size { + Size::new(self.width.into(), self.height.into()) } fn tag(&self) -> tree::Tag { @@ -315,7 +312,12 @@ where } } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { iced_widget::image::layout( renderer, limits, @@ -326,19 +328,20 @@ where ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, - _layout: Layout<'_>, - _cursor_position: Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, + event: &Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + viewport: &Rectangle, + ) { let state = tree.state.downcast_mut::(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { let elapsed = now.duration_since(state.current.started); if elapsed > state.current.frame.delay { @@ -346,15 +349,14 @@ where state.current = self.frames.frames[state.index].clone().into(); - shell.request_redraw(window::RedrawRequest::At(now + state.current.frame.delay)); + shell + .request_redraw_at(window::RedrawRequest::At(*now + state.current.frame.delay)); } else { let remaining = state.current.frame.delay - elapsed; - shell.request_redraw(window::RedrawRequest::At(now + remaining)); + shell.request_redraw_at(window::RedrawRequest::At(*now + remaining)); } } - - event::Status::Ignored } fn draw( diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index a7e42759..8ed4c0ec 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -17,7 +17,7 @@ use taffy::{AlignContent, TaffyTree}; pub fn resolve( renderer: &Renderer, limits: &Limits, - items: &[Element<'_, Message>], + items: &mut [Element<'_, Message>], assignments: &[Assignment], width: Length, height: Length, @@ -37,9 +37,13 @@ pub fn resolve( let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); // Attach widgets as child nodes. - for ((child, assignment), tree) in items.iter().zip(assignments.iter()).zip(tree.iter_mut()) { + for ((child, assignment), tree) in items + .iter_mut() + .zip(assignments.iter()) + .zip(tree.iter_mut()) + { // Calculate the dimensions of the item. - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -172,12 +176,12 @@ pub fn resolve( for (((leaf, child), node), tree) in leafs .into_iter() - .zip(items.iter()) + .zip(items.iter_mut()) .zip(nodes.iter_mut()) .zip(tree) { if let Ok(leaf_layout) = taffy.layout(leaf) { - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 0aca7943..f88dfc2a 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -127,7 +127,7 @@ impl Widget for Grid< } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -141,7 +141,7 @@ impl Widget for Grid< super::layout::resolve( renderer, &limits, - &self.children, + &mut self.children, &self.assignments, self.width, self.height, @@ -156,19 +156,19 @@ impl Widget for Grid< } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -178,25 +178,25 @@ impl Widget for Grid< }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((child, state), c_layout)| { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor, renderer, @@ -204,8 +204,7 @@ impl Widget for Grid< shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -264,11 +263,19 @@ impl Widget for Grid< fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index b0957d68..695c8405 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -157,7 +157,7 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, limits: &iced_core::layout::Limits, @@ -165,7 +165,7 @@ impl Widget let child_tree = &mut tree.children[0]; let child = self .header_bar_inner - .as_widget() + .as_widget_mut() .layout(child_tree, renderer, limits); iced_core::layout::Node::with_children(child.size(), vec![child]) } @@ -193,20 +193,20 @@ impl Widget ); } - fn on_event( + fn update( &mut self, state: &mut tree::Tree, - event: iced_core::Event, + event: &iced_core::Event, layout: iced_core::Layout<'_>, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) -> iced_core::event::Status { + ) { let child_state = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().on_event( + self.header_bar_inner.as_widget_mut().update( child_state, event, child_layout, @@ -238,7 +238,7 @@ impl Widget } fn operate( - &self, + &mut self, state: &mut tree::Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, @@ -246,16 +246,20 @@ impl Widget ) { let child_tree = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); - self.header_bar_inner - .as_widget() - .operate(child_tree, child_layout, renderer, operation); + self.header_bar_inner.as_widget_mut().operate( + child_tree, + child_layout, + renderer, + operation, + ); } fn overlay<'b>( &'b mut self, state: &'b mut tree::Tree, - layout: iced_core::Layout<'_>, + layout: iced_core::Layout<'b>, renderer: &crate::Renderer, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let child_tree = &mut state.children[0]; @@ -264,6 +268,7 @@ impl Widget child_tree, child_layout, renderer, + viewport, translation, ) } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 6c6a9f08..031b4b0c 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -15,7 +15,7 @@ pub use handle::{Data, Handle, from_path, from_raster_bytes, from_raster_pixels, use crate::Element; use derive_setters::Setters; use iced::widget::{Image, Svg}; -use iced::{ContentFit, Length, Rectangle}; +use iced::{ContentFit, Length, Radians, Rectangle}; use iced_core::Rotation; /// Create an [`Icon`] from a pre-existing [`Handle`] @@ -125,17 +125,22 @@ pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectan renderer, iced_core::svg::Svg::new(handle), icon_bounds, + icon_bounds, ), Data::Image(handle) => { iced_core::image::Renderer::draw_image( renderer, - handle, - iced_core::image::FilterMethod::Linear, + iced_core::Image { + handle, + filter_method: iced_core::image::FilterMethod::Linear, + rotation: Radians(0.), + border_radius: [0.0; 4].into(), + opacity: 1.0, + snap: true, + }, + icon_bounds, icon_bounds, - iced_core::Radians::from(0), - 1.0, - [0.0; 4], ); } } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 3d468b20..c8e49e04 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -3,7 +3,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree}; +use iced_core::widget::{Id, Operation, Tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; @@ -65,29 +65,30 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -99,18 +100,18 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], event, layout @@ -169,8 +170,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -181,6 +183,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 74521b3d..110af518 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -172,7 +172,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -181,7 +181,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -190,18 +190,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -257,11 +257,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } fn drag_destinations( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index a3dedd96..49df998a 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -6,7 +6,7 @@ use iced_widget::container::Catalog; use crate::{ Apply, Element, theme, - widget::{container, divider, vertical_space}, + widget::{container, divider, space::vertical}, }; #[inline] @@ -65,7 +65,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { // Ensure a minimum height of 32. let list_item = iced::widget::row![ container(item).align_y(iced::Alignment::Center), - vertical_space().height(iced::Length::Fixed(32.)) + vertical().height(iced::Length::Fixed(32.)) ] .padding(this.list_item_padding) .align_y(iced::Alignment::Center); diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 8eb08d4e..4a58f13a 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -57,11 +57,11 @@ pub fn resolve<'a, E, Message, Renderer>( padding: Padding, spacing: f32, align_items: Alignment, - items: &[E], + items: &mut [E], tree: &mut [&mut Tree], ) -> Node where - E: std::borrow::Borrow>, + E: std::borrow::BorrowMut>, Renderer: renderer::Renderer, { let limits = limits.shrink(padding); @@ -69,7 +69,7 @@ where let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec = Vec::with_capacity(items.len()); @@ -78,8 +78,8 @@ where if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter().zip(tree.iter_mut()) { - let child = child.borrow(); + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -92,7 +92,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -102,8 +102,8 @@ where cross = fill_cross; } - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { - let child = child.borrow(); + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -129,7 +129,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -146,8 +146,8 @@ where let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { - let child = child.borrow(); + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -180,7 +180,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); if align_items != Alignment::Center { cross = cross.max(axis.cross(layout.size())); @@ -231,7 +231,7 @@ pub fn resolve_wrapper<'a, Message>( padding: Padding, spacing: f32, align_items: Alignment, - items: &[&RcElementWrapper], + items: &mut [&mut RcElementWrapper], tree: &mut [&mut Tree], ) -> Node { let limits = limits.shrink(padding); @@ -239,7 +239,7 @@ pub fn resolve_wrapper<'a, Message>( let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec = Vec::with_capacity(items.len()); @@ -248,7 +248,7 @@ pub fn resolve_wrapper<'a, Message>( if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter().zip(tree.iter_mut()) { + for (child, tree) in items.into_iter().zip(tree.iter_mut()) { let c_size = child.size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -271,7 +271,7 @@ pub fn resolve_wrapper<'a, Message>( cross = fill_cross; } - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -314,7 +314,7 @@ pub fn resolve_wrapper<'a, Message>( let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index bbbb4a2b..05fcc133 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -26,7 +26,7 @@ use crate::{ }, }; -use iced::{Point, Shadow, Vector, window}; +use iced::{Point, Shadow, Vector, event::Status, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, @@ -533,14 +533,14 @@ where menu_roots_children(&self.menu_roots) } - fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); - let children = self + let mut children = self .menu_roots - .iter() - .map(|root| &root.item) + .iter_mut() + .map(|root| &mut root.item) .collect::>(); // the first children of the tree are the menu roots items let mut tree_children = tree @@ -555,28 +555,28 @@ where self.padding, self.spacing, Alignment::Center, - &children, + &mut children, &mut tree_children, ) } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: event::Event, + event: &event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { use event::Event::{Mouse, Touch}; use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; - let root_status = process_root_events( + process_root_events( &mut self.menu_roots, view_cursor, tree, @@ -638,7 +638,7 @@ where }); if !create_popup { - return event::Status::Ignored; + return; } #[cfg(all( feature = "multi-window", @@ -665,8 +665,6 @@ where } _ => (), } - - root_status } fn draw( @@ -704,6 +702,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }; renderer.fill_quad(path_quad, styling.path); @@ -731,8 +730,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, _renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all( @@ -799,18 +799,16 @@ fn process_root_events( clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, -) -> event::Status -where -{ +) { menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((root, t), lo)| { // assert!(t.tag == tree::Tag::stateless()); - root.item.on_event( + root.item.update( &mut t.children[root.index], - event.clone(), + event, lo, view_cursor, renderer, @@ -818,6 +816,5 @@ where shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c455cd13..d52c929d 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -310,7 +310,7 @@ pub(crate) struct MenuState { } impl MenuState { pub(super) fn layout( - &self, + &mut self, overlay_offset: Vector, slice: MenuSlice, renderer: &crate::Renderer, @@ -329,8 +329,8 @@ impl MenuState { // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; let child_nodes = self.menu_bounds.child_positions[start_index..=end_index] - .iter() - .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) + .iter_mut() + .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter_mut()) .zip(menu_tree[start_index..=end_index].iter()) .map(|((cp, size), mt)| { let mut position = *cp; @@ -347,7 +347,11 @@ impl MenuState { let limits = Limits::new(size, size); mt.item - .layout(&mut tree[mt.index], renderer, &limits) + .element + .with_data_mut(|e| { + e.as_widget_mut() + .layout(&mut tree[mt.index], renderer, &limits) + }) .move_to(Point::new(0.0, position + self.scroll_offset)) }) .collect::>(); @@ -360,7 +364,7 @@ impl MenuState { overlay_offset: Vector, index: usize, renderer: &crate::Renderer, - menu_tree: &MenuTree, + menu_tree: &mut MenuTree, tree: &mut Tree, ) -> Node { // viewport space children bounds @@ -499,7 +503,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } else { self.depth }] - .iter() + .iter_mut() .enumerate() .filter(|ms| self.is_overlay || ms.0 < 1) .fold( @@ -545,15 +549,15 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, - event: event::Event, + event: &event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> (Option<(usize, MenuState)>, event::Status) { + ) -> Option<(usize, MenuState)> { use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -569,7 +573,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .inner .with_data(|data| data.open || data.active_root.len() <= self.depth) { - return (None, Ignored); + return None; } let viewport = layout.bounds(); @@ -583,7 +587,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }; let menu_status = process_menu_events( self, - event.clone(), + &event, view_cursor, renderer, clipboard, @@ -602,25 +606,28 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { self.main_offset as f32, ); - let ret = match event { - Mouse(WheelScrolled { delta }) => { - process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset) - .merge(menu_status) - } + match event { + Mouse(WheelScrolled { delta }) => process_scroll_events( + self, + shell, + *delta, + overlay_cursor, + viewport_size, + overlay_offset, + ), Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => { self.tree.inner.with_data_mut(|data| { data.pressed = true; data.view_cursor = view_cursor; }); - Captured } Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { - let view_cursor = Cursor::Available(position); + let view_cursor = Cursor::Available(*position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; if !self.is_overlay && !view_cursor.is_over(viewport) { - return (None, menu_status); + return None; } let (new_root, status) = process_overlay_events( @@ -634,7 +641,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); - return (new_root, status.merge(menu_status)); + return new_root; } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { @@ -694,23 +701,19 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } state.reset(); - return Captured; } } // close all menus when clicking inside the menu bar if self.bar_bounds.contains(overlay_cursor) { state.reset(); - Captured - } else { - menu_status } }) } - _ => menu_status, + _ => {} }; - (None, ret) + None } #[allow(unused_results, clippy::too_many_lines)] @@ -734,7 +737,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let render_bounds = if self.is_overlay { Rectangle::new(Point::ORIGIN, viewport.size()) } else { - Rectangle::new(Point::ORIGIN, Size::INFINITY) + Rectangle::new(Point::ORIGIN, Size::INFINITE) }; let styling = theme.appearance(&self.style); @@ -796,6 +799,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { color: styling.border_color, }, shadow: Shadow::default(), + snap: true, }; let menu_color = styling.background; r.fill_quad(menu_quad, menu_color); @@ -815,6 +819,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ..Default::default() }, shadow: Shadow::default(), + snap: true, }; r.fill_quad(path_quad, styling.path); @@ -867,17 +872,16 @@ impl overlay::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.on_event(event, layout, cursor, renderer, clipboard, shell) - .1 + ) { + self.update(event, layout, cursor, renderer, clipboard, shell); } fn draw( @@ -903,7 +907,7 @@ impl Widget Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); + ) { + let new_root = self.update(event, layout, cursor, renderer, clipboard, shell); #[cfg(all( feature = "multi-window", @@ -997,7 +1001,7 @@ impl Widget Widget Widget Rectangle { Rectangle { x: rect.x - padding.left, y: rect.y - padding.top, - width: rect.width + padding.horizontal(), - height: rect.height + padding.vertical(), + width: rect.width + padding.x(), + height: rect.height + padding.y(), } } @@ -1274,15 +1277,13 @@ pub(super) fn init_root_popup_menu( #[allow(clippy::too_many_arguments)] fn process_menu_events( menu: &mut Menu, - event: event::Event, + event: &event::Event, view_cursor: Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, overlay_offset: Vector, -) -> event::Status { - use event::Status; - +) { let my_state = &mut menu.tree; let menu_roots = match &mut menu.menu_roots { Cow::Borrowed(_) => panic!(), @@ -1290,15 +1291,15 @@ fn process_menu_events( }; my_state.inner.with_data_mut(|state| { if state.active_root.len() <= menu.depth { - return event::Status::Ignored; + return; } let Some(hover) = state.menu_states.last_mut() else { - return Status::Ignored; + return; }; let Some(hover_index) = hover.index else { - return Status::Ignored; + return; }; let mt = state.active_root.iter().skip(1).fold( @@ -1321,7 +1322,7 @@ fn process_menu_events( let child_layout = Layout::new(&child_node); // process only the last widget - mt.item.on_event( + mt.item.update( tree, event, child_layout, @@ -1330,7 +1331,7 @@ fn process_menu_events( clipboard, shell, &Rectangle::default(), - ) + ); }) } @@ -1561,12 +1562,12 @@ where fn process_scroll_events( menu: &mut Menu<'_, Message>, + shell: &mut Shell<'_, Message>, delta: mouse::ScrollDelta, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, -) -> event::Status -where +) where Message: Clone, { use event::Status::{Captured, Ignored}; @@ -1590,12 +1591,12 @@ where // update if state.menu_states.is_empty() { - return Ignored; + return; } else if state.menu_states.len() == 1 { let last_ms = &mut state.menu_states[0]; if last_ms.index.is_none() { - return Captured; + return; } let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); @@ -1616,7 +1617,8 @@ where .children_bounds .contains(overlay_cursor) { - return Captured; + shell.capture_event(); + return; } // scroll the second last one @@ -1632,8 +1634,8 @@ where last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; } } - Captured - }) + shell.capture_event(); + }); } #[allow(clippy::pedantic)] @@ -1666,11 +1668,11 @@ fn get_children_layout( .map(|mt| { mt.item .element - .with_data(|w| match w.as_widget().size().height { + .with_data_mut(|w| match w.as_widget_mut().size().height { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w - .as_widget() + .as_widget_mut() .layout( &mut tree[mt.index], renderer, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 15dd5810..bd182b9c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -253,13 +253,16 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ widget::text(l).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); } let menu_button = menu_button(items).on_press(action.message()); @@ -273,13 +276,16 @@ pub fn menu_items< let mut items = vec![ widget::text(l).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); } let menu_button = menu_button(items); @@ -301,16 +307,21 @@ pub fn menu_items< .width(Length::Fixed(16.0)) .into() } else { - widget::Space::with_width(Length::Fixed(16.0)).into() + widget::space::horizontal() + .width(Length::Fixed(16.0)) + .into() }, - widget::Space::with_width(spacing.space_xxs).into(), + widget::space::horizontal().width(spacing.space_xxs).into(), widget::text(label).align_x(iced::Alignment::Start).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); items.insert(2, widget::icon::icon(icon).size(14).into()); } @@ -325,7 +336,7 @@ pub fn menu_items< RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ widget::text(l.clone()).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::icon::from_name("pan-end-symbolic") .size(16) .icon() diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 202173ef..30b75a10 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -60,7 +60,7 @@ pub use iced::widget::{ComboBox, combo_box}; pub use iced::widget::{Container, container}; #[doc(inline)] -pub use iced::widget::{Space, horizontal_space, vertical_space}; +pub use iced::widget::{Space, space}; #[doc(inline)] pub use iced::widget::{Image, image}; @@ -175,47 +175,47 @@ pub use dialog::{Dialog, dialog}; pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::widget::{Rule, horizontal_rule}; + use iced::{widget::Rule, widget::rule}; /// Horizontal divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(1).class(crate::theme::Rule::Default) + rule::horizontal(1).class(crate::theme::Rule::Default) } /// Horizontal divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(1).class(crate::theme::Rule::LightDivider) + rule::horizontal(1).class(crate::theme::Rule::LightDivider) } /// Horizontal divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(4).class(crate::theme::Rule::HeavyDivider) + rule::horizontal(4).class(crate::theme::Rule::HeavyDivider) } } /// Vertical variant of a divider. pub mod vertical { - use iced::widget::{Rule, vertical_rule}; + use iced::widget::{Rule, rule}; /// Vertical divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(1).class(crate::theme::Rule::Default) + rule::vertical(1).class(crate::theme::Rule::Default) } /// Vertical divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(4).class(crate::theme::Rule::LightDivider) + rule::vertical(4).class(crate::theme::Rule::LightDivider) } /// Vertical divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(10).class(crate::theme::Rule::HeavyDivider) + rule::vertical(10).class(crate::theme::Rule::HeavyDivider) } } } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 140385bc..ad6f9206 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -180,5 +180,6 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), + snap: true, } } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index ddc31455..951b3757 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -3,6 +3,7 @@ //! A container which displays an overlay when a popup widget is attached. +use iced::widget; use iced_core::event::{self, Event}; use iced_core::layout; use iced_core::mouse; @@ -33,6 +34,7 @@ pub enum Position { /// A container which displays overlays when a popup widget is assigned. #[must_use] pub struct Popover<'a, Message, Renderer> { + id: widget::Id, content: Element<'a, Message, crate::Theme, Renderer>, modal: bool, popup: Option>, @@ -43,6 +45,7 @@ pub struct Popover<'a, Message, Renderer> { impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { pub fn new(content: impl Into>) -> Self { Self { + id: widget::Id::unique(), content: content.into(), modal: false, popup: None, @@ -51,6 +54,13 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { } } + /// Set the Id + #[inline] + pub fn id(mut self, id: widget::Id) -> Self { + self.id = id; + self + } + /// A modal popup intercepts user inputs while a popup is active. #[inline] pub fn modal(mut self, modal: bool) -> Self { @@ -83,6 +93,14 @@ impl Widget where Renderer: iced_core::Renderer, { + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: widget::Id) { + self.id = id; + } + fn children(&self) -> Vec { if let Some(popup) = &self.popup { vec![Tree::new(&self.content), Tree::new(popup)] @@ -104,42 +122,53 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let tree = content_tree_mut(tree); - self.content.as_widget().layout(tree, renderer, limits) + self.content.as_widget_mut().layout(tree, renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { - self.content - .as_widget() - .operate(content_tree_mut(tree), layout, renderer, operation); + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { if self.popup.is_some() { if self.modal { if matches!(event, Event::Mouse(_) | Event::Touch(_)) { - return event::Status::Captured; + shell.capture_event(); + return; } } else if let Some(on_close) = self.on_close.as_ref() { if matches!( @@ -153,7 +182,7 @@ where } } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( content_tree_mut(tree), event, layout, @@ -209,8 +238,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { if let Some(popup) = &mut self.popup { @@ -248,6 +278,7 @@ where content_tree_mut(tree), layout, renderer, + viewport, translation, ) } @@ -312,7 +343,7 @@ where let limits = layout::Limits::new(Size::UNIT, bounds); let node = self .content - .as_widget() + .as_widget_mut() .layout(self.tree, renderer, &limits); match self.position { Position::Center => { @@ -353,27 +384,28 @@ where operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(self.tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { if self.modal && matches!(event, Event::Mouse(_) | Event::Touch(_)) && !cursor_position.is_over(layout.bounds()) { - return event::Status::Captured; + shell.capture_event(); + return; } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( self.tree, event, layout, @@ -389,7 +421,6 @@ where &self, layout: Layout<'_>, cursor_position: mouse::Cursor, - viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { if self.modal && !cursor_position.is_over(layout.bounds()) { @@ -400,7 +431,7 @@ where self.tree, layout, cursor_position, - viewport, + &layout.bounds(), renderer, ) } @@ -427,12 +458,16 @@ where fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &Renderer, ) -> Option> { - self.content - .as_widget_mut() - .overlay(self.tree, layout, renderer, Default::default()) + self.content.as_widget_mut().overlay( + self.tree, + layout, + renderer, + &layout.bounds(), + Default::default(), + ) } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index ebb75ee2..831e9460 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -175,7 +175,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,20 +186,20 @@ where |_| layout::Node::new(Size::new(self.size, self.size)), |limits| { self.label - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.label.as_widget().operate( + self.label.as_widget_mut().operate( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -207,20 +207,20 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let status = self.label.as_widget_mut().on_event( + ) { + self.label.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout.children().nth(1).unwrap(), cursor, renderer, @@ -229,22 +229,19 @@ where viewport, ); - if status == event::Status::Ignored { + if !shell.is_event_captured() { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } _ => {} } - - event::Status::Ignored - } else { - status } } @@ -359,14 +356,16 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, + viewport, translation, ) } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 632578ff..b3066ecb 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -204,7 +204,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -221,7 +221,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -230,18 +230,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -290,11 +290,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } fn drag_destinations( diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 541862cd..02fa4329 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -18,10 +18,10 @@ pub fn rectangle_tracker_subscription< >( id: I, ) -> Subscription<(I, RectangleUpdate)> { - Subscription::run_with_id( - id, - stream::unfold(State::Ready, move |state| start_listening(id, state)), - ) + Subscription::run_with(id, |id| { + let id = *id; + stream::unfold(State::Ready, move |state| start_listening(id, state)) + }) } pub enum State { diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index fbc2df9e..0c7fbad3 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -6,7 +6,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree, tree}; +use iced_core::widget::{Id, Operation, Tree, tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( @@ -89,7 +89,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -98,7 +98,7 @@ where let unrestricted_size = self.size.unwrap_or_else(|| { let node = self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, &Limits::NONE); node.size() }); @@ -115,22 +115,23 @@ where let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -142,17 +143,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); if state.needs_update { @@ -166,7 +167,7 @@ where state.needs_update = false; } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout @@ -225,8 +226,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -237,6 +239,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 0e1af1d0..162d1d21 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -23,7 +23,7 @@ use iced::{ event, keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; -use iced_core::text::{Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; +use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; use iced_core::widget::{self, operation, tree}; use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text}; @@ -265,22 +265,33 @@ where } } - let text = Text { - content: text.as_ref(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::None, - ellipsize: Ellipsize::None, - line_height: self.line_height, - }; - if let Some(paragraph) = state.paragraphs.get_mut(key) { + let text = Text { + content: text.as_ref(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + ellipsize: Ellipsize::default(), + }; paragraph.update(text); } else { + let text = Text { + content: text.to_string(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + ellipsize: Ellipsize::default(), + }; state.paragraphs.insert(key, crate::Plain::new(text)); } } @@ -441,7 +452,7 @@ where } /// Item the previous item in the widget. - fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { + fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state).rev(); @@ -455,7 +466,8 @@ where } state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } break; @@ -464,24 +476,28 @@ where if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - return event::Status::Captured; + shell.capture_event(); + return; } } Item::NextButton => { if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - return event::Status::Captured; + shell.capture_event(); + return; } } Item::None => { if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - return event::Status::Captured; + shell.capture_event(); + return; } else if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -489,11 +505,10 @@ where } state.focused_item = Item::None; - event::Status::Ignored } /// Item the next item in the widget. - fn focus_next(&mut self, state: &mut LocalState) -> event::Status { + fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state); @@ -506,7 +521,8 @@ where } state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } break; @@ -515,24 +531,28 @@ where if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - return event::Status::Captured; + shell.capture_event(); + return; } } Item::PrevButton => { if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - return event::Status::Captured; + shell.capture_event(); + return; } } Item::None => { if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - return event::Status::Captured; + shell.capture_event(); + return; } else if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -540,7 +560,6 @@ where } state.focused_item = Item::None; - event::Status::Ignored } fn iterate_visible_tabs<'b>( @@ -595,12 +614,12 @@ where icon_spacing = f32::from(self.button_spacing); let paragraph = entry.or_insert_with(|| { crate::Plain::new(Text { - content: text.as_ref(), + content: text.to_string(), // TODO should we just use String at this point? size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, + bounds: Size::INFINITE, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), ellipsize: Ellipsize::default(), @@ -888,7 +907,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -902,17 +921,17 @@ where } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - mut event: Event, + mut event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, - ) -> event::Status { + ) { let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); @@ -941,7 +960,8 @@ where "tab drag source finished id={:?}", my_id ); - return event::Status::Captured; + shell.capture_event(); + return; } } DndEvent::Offer( @@ -1137,8 +1157,8 @@ where }); let (maybe_msg, ret) = state.dnd_state.on_data_received( - mem::take(mime_type), - mem::take(data), + mime_type.clone(), + data.clone(), None:: Message>, on_drop, ); @@ -1160,10 +1180,11 @@ where } if let Some(on_reorder) = self.on_reorder.as_ref() { shell.publish(on_reorder(event)); - return event::Status::Captured; + shell.capture_event(); + return; } } - return ret; + return; } } _ => {} @@ -1175,7 +1196,7 @@ where match event { Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(id); + state.fingers_pressed.insert(*id); } Event::Touch(touch::Event::FingerLifted { id, .. }) => { @@ -1252,7 +1273,8 @@ where || (touch_lifted(&event) && fingers_pressed == 1)) { shell.publish(on_close(key)); - return event::Status::Captured; + shell.capture_event(); + return; } if self.on_middle_press.is_none() { @@ -1263,7 +1285,8 @@ where { if state.middle_clicked == Some(Item::Tab(key)) { shell.publish(on_close(key)); - return event::Status::Captured; + shell.capture_event(); + return; } state.middle_clicked = None; @@ -1315,7 +1338,8 @@ where state.set_focused(); state.focused_item = Item::Tab(key); state.pressed_item = None; - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1336,7 +1360,8 @@ where }); shell.publish(on_context(key)); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1347,7 +1372,8 @@ where state.middle_clicked = Some(Item::Tab(key)); if let Some(on_middle_press) = self.on_middle_press.as_ref() { shell.publish(on_middle_press(key)); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1374,7 +1400,7 @@ where ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { let mut activate_key = None; - if y < 0.0 { + if *y < 0.0 { let mut prev_key = Entity::null(); for key in self.model.order.iter().copied() { @@ -1386,7 +1412,7 @@ where prev_key = key; } } - } else if y > 0.0 { + } else if *y > 0.0 { let mut buttons = self.model.order.iter().copied(); while let Some(key) = buttons.next() { if self.model.is_active(key) { @@ -1405,7 +1431,8 @@ where shell.publish(on_activate(key)); state.set_focused(); state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1424,7 +1451,7 @@ where if is_pressed(&event) { state.unfocus(); state.pressed_item = None; - return event::Status::Ignored; + return; } } else if is_lifted(&event) { state.pressed_item = None; @@ -1452,7 +1479,8 @@ where position, clipboard, ) { - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1475,12 +1503,10 @@ where }) = event { state.focused_visible = true; - return if modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state) + return if *modifiers == keyboard::Modifiers::SHIFT { + self.focus_previous(state, shell) } else if modifiers.is_empty() { - self.focus_next(state) - } else { - event::Status::Ignored + self.focus_next(state, shell) }; } @@ -1524,24 +1550,23 @@ where Item::None | Item::Set => (), } - return event::Status::Captured; + shell.capture_event(); + return; } } } - - event::Status::Ignored } fn operate( - &self, + &mut self, tree: &mut Tree, - _layout: Layout<'_>, + layout: Layout<'_>, _renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { let state = tree.state.downcast_mut::(); - operation.focusable(state, Some(&self.id.0)); - operation.custom(state, Some(&self.id.0)); + operation.focusable(Some(&self.id.0), layout.bounds(), state); + operation.custom(Some(&self.id.0), layout.bounds(), state); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { @@ -1616,6 +1641,7 @@ where bounds, border: appearance.border, shadow: Shadow::default(), + snap: true, }, background, ); @@ -1644,6 +1670,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background_appearance .background @@ -1692,6 +1719,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background_appearance .background @@ -1747,6 +1775,7 @@ where bounds, border: Border::default(), shadow: Shadow::default(), + snap: true, }, { let theme = crate::theme::active(); @@ -1842,6 +1871,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.active.text_color, ); @@ -1878,6 +1908,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, divider_background, ); @@ -1910,6 +1941,7 @@ where button_appearance.border }, shadow: Shadow::default(), + snap: true, }, status_appearance .background @@ -2069,8 +2101,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: iced_core::Layout<'_>, + layout: iced_core::Layout<'b>, _renderer: &Renderer, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); @@ -2662,6 +2695,7 @@ fn draw_drop_indicator( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color(color), ); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index d62bbc99..a17f2071 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -5,10 +5,11 @@ use std::borrow::Cow; use crate::{ Element, theme, - widget::{FlexRow, Row, column, container, flex_row, horizontal_space, row, text}, + widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; +use iced_widget::space; use taffy::AlignContent; /// A settings item aligned in a row @@ -25,7 +26,7 @@ pub fn item<'a, Message: 'static>( ) -> Row<'a, Message> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), - horizontal_space().into(), + space::horizontal().into(), widget, ]) } diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 9ad81b4d..833e90b8 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -313,6 +313,7 @@ fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { background: None, border, shadow: Shadow::default(), + snap: true, } } diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 0ad92166..85b5cfce 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -131,6 +131,7 @@ where ..Default::default() }, shadow: Default::default(), + snap: true, } })) .apply(widget::mouse_area) diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index c0207f06..79107074 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -192,6 +192,7 @@ where ..Default::default() }, shadow: Default::default(), + snap: true, } })) .apply(widget::mouse_area) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index e98d4cfa..5b6a53f3 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -699,7 +699,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -711,7 +711,7 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); - let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITY); + let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITE); let value_paragraph = &mut state.value; let v = self.value.to_string(); value_paragraph.update(Text { @@ -723,8 +723,8 @@ where font, bounds, size: iced::Pixels(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -743,8 +743,8 @@ where self.width, self.padding, self.size, - self.leading_icon.as_ref(), - self.trailing_icon.as_ref(), + self.leading_icon.as_mut(), + self.trailing_icon.as_mut(), self.line_height, self.label.as_deref(), self.helper_text.as_deref(), @@ -780,24 +780,25 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, - _layout: Layout<'_>, - _renderer: &crate::Renderer, - operation: &mut dyn Operation<()>, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn Operation, ) { + operation.container(Some(&self.id), layout.bounds()); let state = tree.state.downcast_mut::(); - operation.custom(state, Some(&self.id)); - operation.focusable(state, Some(&self.id)); - operation.text_input(state, Some(&self.id)); + operation.focusable(Some(&self.id), layout.bounds(), state); + operation.text_input(Some(&self.id), layout.bounds(), state); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let mut layout_ = Vec::with_capacity(2); @@ -823,24 +824,24 @@ where .filter_map(|((child, state), layout)| { child .as_widget_mut() - .overlay(state, layout, renderer, translation) + .overlay(state, layout, renderer, viewport, translation) }) .collect::>(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + 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()); @@ -877,9 +878,9 @@ where // Enable custom buttons defined on the trailing icon position to be handled. if !self.is_editable_variant { if let Some(trailing_layout) = trailing_icon_layout { - let res = trailing_icon.as_widget_mut().on_event( + let res = trailing_icon.as_widget_mut().update( tree, - event.clone(), + event, trailing_layout, cursor_position, renderer, @@ -888,8 +889,8 @@ where viewport, ); - if res == event::Status::Captured { - return res; + if shell.is_event_captured() { + return; } } } @@ -1133,8 +1134,8 @@ pub fn layout( width: Length, padding: Padding, size: Option, - leading_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, - trailing_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, + leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, + trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, line_height: text::LineHeight, label: Option<&str>, helper_text: Option<&str>, @@ -1148,7 +1149,7 @@ pub fn layout( 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 text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); let state = tree.state.downcast_mut::(); let label_paragraph = &mut state.label; label_paragraph.update(Text { @@ -1156,8 +1157,8 @@ pub fn layout( 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, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1186,7 +1187,7 @@ pub fn layout( 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( + let icon_node = icon.as_widget_mut().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1201,7 +1202,7 @@ pub fn layout( 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( + let icon_node = icon.as_widget_mut().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1214,7 +1215,7 @@ pub fn layout( let text_limits = limits .width(width) .height(line_height.to_absolute(text_size.into())); - let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); + let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); let text_node = layout::Node::new( text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0), ) @@ -1266,9 +1267,9 @@ pub fn layout( } else { let limits = limits .width(width) - .height(text_input_height + padding.vertical()) + .height(text_input_height + padding.y()) .shrink(padding); - let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top)); @@ -1286,7 +1287,7 @@ pub fn layout( .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 text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); let state = tree.state.downcast_mut::(); let helper_text_paragraph = &mut state.helper_text; helper_text_paragraph.update(Text { @@ -1294,8 +1295,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(helper_text_size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: helper_text_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1332,7 +1333,7 @@ pub fn layout( #[allow(clippy::cast_possible_truncation)] pub fn update<'a, Message: Clone + 'static>( id: Option, - event: Event, + event: &Event, text_layout: Layout<'_>, edit_button_layout: Option>, cursor: mouse::Cursor, @@ -1357,7 +1358,7 @@ pub fn update<'a, Message: Clone + 'static>( layout: Layout<'_>, manage_value: bool, drag_threshold: f32, -) -> event::Status { +) { let update_cache = |state, value| { replace_paragraph( state, @@ -1420,7 +1421,8 @@ pub fn update<'a, Message: Clone + 'static>( }); } - return event::Status::Captured; + shell.capture_event(); + return; } let target = cursor_position.x - text_layout.bounds().x; @@ -1461,13 +1463,15 @@ pub fn update<'a, Message: Clone + 'static>( if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value) { state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position)); - return event::Status::Captured; + shell.capture_event(); + return; } // clear selection and place cursor at click position update_cache(state, value); state.setting_selection(value, text_layout.bounds(), target); state.dragging_state = None; - return event::Status::Captured; + shell.capture_event(); + return; } (None, click::Kind::Single, _) => { state.setting_selection(value, text_layout.bounds(), target); @@ -1528,7 +1532,8 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); - return event::Status::Captured; + shell.capture_event(); + return; } else { state.unfocus(); @@ -1551,12 +1556,10 @@ pub fn update<'a, Message: Clone + 'static>( } } state.dragging_state = None; - - return if cursor.is_over(layout.bounds()) { - event::Status::Captured - } else { - event::Status::Ignored - }; + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + return; } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { @@ -1573,7 +1576,8 @@ pub fn update<'a, Message: Clone + 'static>( .cursor .select_range(state.cursor.start(value), position); - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state { @@ -1583,7 +1587,7 @@ pub fn update<'a, Message: Clone + 'static>( if distance >= drag_threshold { if is_secure { - return event::Status::Ignored; + return; } let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); @@ -1625,7 +1629,8 @@ pub fn update<'a, Message: Clone + 'static>( state.dragging_state = Some(DraggingState::PrepareDnd(start_position)); } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1636,11 +1641,11 @@ pub fn update<'a, Message: Clone + 'static>( .. }) => { let state = state(); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; 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; + return; }; let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -1724,12 +1729,14 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } keyboard::Key::Character("a") | keyboard::Key::Character("A") => { state.cursor.select_all(value); - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} @@ -1737,9 +1744,12 @@ pub fn update<'a, Message: Clone + 'static>( } // Capture keyboard inputs that should be submitted. - if let Some(c) = text.and_then(|t| t.chars().next().filter(|c| !c.is_control())) { + if let Some(c) = text + .as_ref() + .and_then(|t| t.chars().next().filter(|c| !c.is_control())) + { if state.is_read_only || (!manage_value && on_input.is_none()) { - return event::Status::Ignored; + return; }; state.is_pasting = None; @@ -1769,7 +1779,8 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -1902,19 +1913,20 @@ pub fn update<'a, Message: Clone + 'static>( shell.publish(on_unfocus.clone()); } - return event::Status::Ignored; + return; }; } keyboard::Key::Named( keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown, ) => { - return event::Status::Ignored; + return; } _ => {} } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { @@ -1928,31 +1940,30 @@ pub fn update<'a, Message: Clone + 'static>( 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; } _ => {} } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; } Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { - focus.now = now; + focus.now = *now; let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() % 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()), - )); + shell.request_redraw(); } } #[cfg(feature = "wayland")] @@ -1962,7 +1973,8 @@ pub fn update<'a, Message: Clone + 'static>( if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { // TODO: restore value in text input state.dragging_state = None; - return event::Status::Captured; + shell.capture_event(); + return; } } #[cfg(feature = "wayland")] @@ -1974,23 +1986,23 @@ pub fn update<'a, Message: Clone + 'static>( mime_types, surface, }, - )) if rectangle == Some(dnd_id) => { + )) if *rectangle == Some(dnd_id) => { cold(); let state = state(); let is_clicked = text_layout.bounds().contains(Point { - x: x as f32, - y: y as f32, + x: *x as f32, + y: *y as f32, }); let mut accepted = false; - for m in &mime_types { + 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; + let target = *x as f32 - text_layout.bounds().x; state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); // existing logic for setting the selection @@ -2002,16 +2014,17 @@ pub fn update<'a, Message: Clone + 'static>( }; state.cursor.move_to(position.unwrap_or(0)); - return event::Status::Captured; + shell.capture_event(); + return; } } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) - if rectangle == Some(dnd_id) => + if *rectangle == Some(dnd_id) => { let state = state(); - let target = x as f32 - text_layout.bounds().x; + let target = *x as f32 - text_layout.bounds().x; // existing logic for setting the selection let position = if target > 0.0 { update_cache(state, value); @@ -2021,10 +2034,11 @@ pub fn update<'a, Message: Clone + 'static>( }; state.cursor.move_to(position.unwrap_or(0)); - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] - Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => { + 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() { @@ -2033,15 +2047,16 @@ pub fn update<'a, Message: Clone + 'static>( .find(|&&m| mime_types.iter().any(|t| t == m)) else { state.dnd_offer = DndOfferState::None; - return event::Status::Captured; + shell.capture_event(); + return; }; state.dnd_offer = DndOfferState::Dropped; } - return event::Status::Ignored; + return; } #[cfg(feature = "wayland")] - Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != id => {} + Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, @@ -2057,21 +2072,24 @@ pub fn update<'a, Message: Clone + 'static>( state.dnd_offer = DndOfferState::None; } }; - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) - if rectangle == Some(dnd_id) => + 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() { - return event::Status::Captured; + shell.capture_event(); + return; } - let Ok(content) = String::from_utf8(data) else { - return event::Status::Captured; + let Ok(content) = String::from_utf8(data.clone()) else { + shell.capture_event(); + return; }; let mut editor = Editor::new(unsecured_value, &mut state.cursor); @@ -2091,14 +2109,13 @@ pub fn update<'a, Message: Clone + 'static>( unsecured_value }; update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } - return event::Status::Ignored; + return; } _ => {} } - - event::Status::Ignored } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -2212,6 +2229,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.background, ); @@ -2228,6 +2246,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, Background::Color(Color::TRANSPARENT), ); @@ -2245,6 +2264,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.background, ); @@ -2258,8 +2278,8 @@ pub fn draw<'a, Message>( 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, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2353,6 +2373,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, text_color, )), @@ -2403,6 +2424,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.selected_fill, )), @@ -2448,8 +2470,8 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2497,8 +2519,8 @@ pub fn draw<'a, Message>( size: iced::Pixels(helper_text_size), font, bounds: helper_text_layout.bounds().size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, line_height: helper_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2811,6 +2833,14 @@ impl operation::TextInput for State { fn select_all(&mut self) { Self::select_all(self); } + + fn text(&self) -> &str { + todo!() + } + + fn select_range(&mut self, start: usize, end: usize) { + todo!() + } } #[inline(never)] @@ -2876,11 +2906,11 @@ fn replace_paragraph( state.value = crate::Plain::new(Text { font, line_height, - content: &value.to_string(), + content: value.to_string(), bounds, size: text_size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, ellipsize: text::Ellipsize::None, diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 52604592..240e4867 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -45,13 +45,13 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -85,29 +85,29 @@ where } fn operate<'b>( - &'b self, + &'b mut self, state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut state.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut state.children[0], event, layout, @@ -139,8 +139,9 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { //TODO: this hides the overlay of the content during the toast @@ -149,6 +150,7 @@ where &mut state.children[0], layout, renderer, + viewport, translation, ) } else { @@ -201,7 +203,7 @@ where let node = self .element - .as_widget() + .as_widget_mut() .layout(self.state, renderer, &limits); let offset = 15.; @@ -228,16 +230,16 @@ where .draw(self.state, renderer, theme, style, layout, cursor, &bounds); } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell, - ) -> event::Status { - self.element.as_widget_mut().on_event( + ) { + self.element.as_widget_mut().update( self.state, event, layout, @@ -253,22 +255,29 @@ where &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.element - .as_widget() - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + self.element.as_widget().mouse_interaction( + self.state, + layout, + cursor, + &layout.bounds(), + renderer, + ) } fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &Renderer, ) -> Option> { - self.element - .as_widget_mut() - .overlay(self.state, layout, renderer, Default::default()) + self.element.as_widget_mut().overlay( + self.state, + layout, + renderer, + &layout.bounds(), + Default::default(), + ) } } diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 942ffb8b..4153d647 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -73,5 +73,6 @@ pub fn warning_container(theme: &Theme) -> widget::container::Style { offset: iced::Vector::new(0.0, 0.0), blur_radius: 0.0, }, + snap: true, } } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index 5194d5c7..ceb234a9 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -211,7 +211,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -224,22 +224,23 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> self.padding, |renderer, limits| { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -251,17 +252,17 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let status = update( self.id.clone(), event.clone(), @@ -275,22 +276,21 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> &self.on_surface_action, || tree.state.downcast_mut::(), ); - status.merge( - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ), - ) + + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); } #[allow(clippy::too_many_lines)] @@ -359,8 +359,9 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -374,6 +375,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } @@ -451,7 +453,7 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( on_leave: &Message, on_surface_action: &dyn Fn(crate::surface::Action) -> Message, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Touch(touch::Event::FingerLifted { .. }) => { let state = state(); @@ -461,7 +463,8 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( shell.publish(on_leave.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -579,8 +582,6 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( } _ => {} } - - event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -611,6 +612,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -632,6 +634,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -647,6 +650,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background, ); @@ -669,6 +673,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 59c0a376..73e476fa 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -90,7 +90,7 @@ impl Widget for RcElementWrapper { } fn layout( - &self, + &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, limits: &crate::iced_core::layout::Limits, @@ -132,30 +132,31 @@ impl Widget for RcElementWrapper { } fn operate( - &self, + &mut self, state: &mut tree::Tree, layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { - self.element.with_data(|e| { - e.as_widget().operate(state, layout, renderer, operation); + self.element.with_data_mut(|e| { + e.as_widget_mut() + .operate(state, layout, renderer, operation); }); } - fn on_event( + fn update( &mut self, state: &mut tree::Tree, - event: crate::iced::Event, + event: &crate::iced::Event, layout: crate::iced_core::Layout<'_>, cursor: crate::iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn crate::iced_core::Clipboard, shell: &mut crate::iced_core::Shell<'_, M>, viewport: &Rectangle, - ) -> event::Status { + ) { self.element.with_data_mut(|e| { - e.as_widget_mut().on_event( + e.as_widget_mut().update( state, event, layout, cursor, renderer, clipboard, shell, viewport, ) }) @@ -178,15 +179,16 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'a>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: crate::iced_core::Vector, ) -> Option> { assert_eq!(self.element.thread_id, thread::current().id()); Rc::get_mut(&mut self.element.data).and_then(|e| { e.get_mut() .as_widget_mut() - .overlay(state, layout, renderer, translation) + .overlay(state, layout, renderer, viewport, translation) }) } From e8d53b14ea348bd42223ddc40bd2e463f87bf401 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Feb 2026 18:15:22 -0500 Subject: [PATCH 054/168] chore: various fixes and some cleanup --- Cargo.toml | 22 +- examples/applet/Cargo.toml | 2 + examples/applet/src/window.rs | 15 +- examples/application/Cargo.toml | 7 +- examples/application/src/main.rs | 9 +- src/anim.rs | 51 +++ src/app/mod.rs | 30 +- src/applet/mod.rs | 3 +- src/lib.rs | 2 + src/widget/autosize.rs | 6 +- src/widget/button/widget.rs | 1 - src/widget/cards.rs | 587 ++++++++++++++++++++++++++ src/widget/context_menu.rs | 2 +- src/widget/dnd_destination.rs | 4 +- src/widget/dnd_source.rs | 48 +-- src/widget/header_bar.rs | 6 +- src/widget/id_container.rs | 6 +- src/widget/list/column.rs | 1 + src/widget/menu/menu_bar.rs | 12 +- src/widget/menu/menu_inner.rs | 3 +- src/widget/mod.rs | 4 + src/widget/popover.rs | 23 +- src/widget/radio.rs | 2 +- src/widget/responsive_container.rs | 4 +- src/widget/segmented_button/widget.rs | 7 +- src/widget/toggler.rs | 429 ++++++++++++++++++- src/widget/wayland/tooltip/widget.rs | 2 +- 27 files changed, 1181 insertions(+), 107 deletions(-) create mode 100644 src/anim.rs create mode 100644 src/widget/cards.rs diff --git a/Cargo.toml b/Cargo.toml index 62b8ee7c..01b50733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,27 +8,28 @@ rust-version = "1.90" name = "cosmic" [features] -# default = ["dbus-config", "multi-window", "a11y"] -default = [ "debug", +default = [ "winit", "tokio", - # "xdg-portal", - "a11y", - "wgpu", - "single-instance", - "surface-message", + "a11y", "dbus-config", "x11", "wayland", "multi-window", - "about","animated-image","autosize", "dbus-config", "pipewire", "process", "rfd", "desktop", "desktop-systemd-scope", "serde-keycode", "qr_code", "markdown", "highlighter" -] +] # default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget about = [] # Builds support for animated images -animated-image = ["dep:async-fs", "image/gif", "image/webp", "image/png", "tokio?/io-util", "tokio?/fs"] +animated-image = [ + "dep:async-fs", + "image/gif", + "image/webp", + "image/png", + "tokio?/io-util", + "tokio?/fs", +] # XXX autosize should not be used on winit windows unless dialogs autosize = [] applet = [ @@ -155,6 +156,7 @@ tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" zbus = { version = "5.13.2", default-features = false, optional = true } +float-cmp = "0.10.0" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index f97bff44..844ad8ff 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,8 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] +# path = "../../" +branch = "iced-rebase" git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 66b2040a..547863f2 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -13,6 +13,7 @@ pub struct Window { core: Core, popup: Option, example_row: bool, + toggle: bool, selected: Option, } @@ -22,6 +23,7 @@ impl Default for Window { core: Core::default(), popup: None, example_row: false, + toggle: false, selected: None, } } @@ -33,6 +35,7 @@ pub enum Message { ToggleExampleRow(bool), Selected(usize), Surface(cosmic::surface::Action), + Toggle(bool), } impl cosmic::Application for Window { @@ -71,7 +74,6 @@ impl cosmic::Application for Window { Message::ToggleExampleRow(toggled) => { self.example_row = toggled; } - Message::Surface(a) => { return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(a), @@ -80,6 +82,9 @@ impl cosmic::Application for Window { Message::Selected(i) => { self.selected = Some(i); } + Message::Toggle(v) => { + self.toggle = v; + } }; Task::none() } @@ -123,9 +128,9 @@ impl cosmic::Application for Window { "Example row", cosmic::widget::container( toggler(state.example_row) - .on_toggle(|value| Message::ToggleExampleRow(value)), - ) - .height(Length::Fixed(50.)), + .on_toggle(Message::ToggleExampleRow) + .width(Length::Fill), + ), )) .add(popup_dropdown( &["1", "asdf", "hello", "test"], @@ -155,7 +160,7 @@ impl cosmic::Application for Window { "oops".into() } - fn style(&self) -> Option { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 35ff3d30..b1ac1242 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,12 +8,11 @@ default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" +env_logger = "0.11" [dependencies.libcosmic] -path = "../../" +git = "https://github.com/pop-os/libcosmic" +branch = "iced-rebase" features = [ "debug", "winit", diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 45805579..831a47f1 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,8 +54,9 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - // tracing_subscriber::fmt::init(); - // let _ = tracing_log::LogTracer::init(); + + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), @@ -66,9 +67,7 @@ fn main() -> Result<(), Box> { let settings = Settings::default() .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, input)?; - + cosmic::app::run::(settings, input).unwrap(); Ok(()) } diff --git a/src/anim.rs b/src/anim.rs new file mode 100644 index 00000000..3186ff2e --- /dev/null +++ b/src/anim.rs @@ -0,0 +1,51 @@ +use std::time::{Duration, Instant}; + +/// A simple linear interpolation calculation function. +/// p = `percent_complete` in decimal form +#[must_use] +pub fn lerp(start: f32, end: f32, p: f32) -> f32 { + (1.0 - p) * start + p * end +} + +/// A fast smooth interpolation calculation function. +/// p = `percent_complete` in decimal form +#[must_use] +pub fn slerp(start: f32, end: f32, p: f32) -> f32 { + let t = smootherstep(p); + (1.0 - t) * start + t * end +} + +/// utility function which maps a value [0, 1] -> [0, 1] using the smootherstep function +pub fn smootherstep(t: f32) -> f32 { + (6.0 * t.powi(5) - 15.0 * t.powi(4) + 10.0 * t.powi(3)).clamp(0.0, 1.0) +} + +#[derive(Default, Debug)] +pub struct State { + pub last_change: Option, +} + +impl State { + pub fn changed(&mut self, dur: Duration) { + let t = self.t(dur, false); + let diff = dur.mul_f32(t.abs()); + let now = Instant::now(); + self.last_change = Some(now.checked_sub(diff).unwrap_or(now)); + } + + pub fn anim_done(&mut self, dur: Duration) { + if self + .last_change + .is_some_and(|t| Instant::now().duration_since(t) > dur) + { + self.last_change = None; + } + } + + pub fn t(&self, dur: Duration, forward: bool) -> f32 { + let res = self.last_change.map_or(1., |t| { + Instant::now().duration_since(t).as_millis() as f32 / dur.as_millis() as f32 + }); + if forward { res } else { 1. - res } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 1287dc27..abda71c1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -12,6 +12,7 @@ use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; use iced::application::BootFn; +use iced_core::Widget; pub mod cosmic; pub mod settings; @@ -93,6 +94,7 @@ pub(crate) fn iced_settings( pub(crate) struct BootDataInner { pub flags: A::Flags, pub core: Core, + pub settings: window::Settings, } pub(crate) struct BootData(pub Rc>>>); @@ -102,8 +104,23 @@ impl BootFn, crate::Action (cosmic::Cosmic, iced::Task>) { let mut data = self.0.borrow_mut(); - let data = data.take().unwrap(); - cosmic::Cosmic::::init((data.core, data.flags)) + let mut data = data.take().unwrap(); + let mut tasks = Vec::new(); + #[cfg(feature = "multi-window")] + if data.core.main_window_id().is_some() { + let window_task = iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + window::Id::RESERVED, + data.settings, + channel, + )) + }); + data.core.set_main_window_id(Some(window::Id::RESERVED)); + tasks.push(window_task.discard()); + } + let (a, t) = cosmic::Cosmic::::init((data.core, data.flags)); + tasks.push(t); + (a, Task::batch(tasks)) } } /// Launch a COSMIC application with the given [`Settings`]. @@ -127,6 +144,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings.clone(), })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -147,10 +165,11 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res // app = app.window(window_settings); core.main_window = Some(iced_core::window::Id::RESERVED); } - let mut app = iced::daemon( + let app = iced::daemon( BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -240,6 +259,7 @@ where BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings.clone(), })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -263,6 +283,7 @@ where BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -700,7 +721,7 @@ impl ApplicationExt for App { [0, 0, 0, 0] }) .into(), - ) + ); } else { //TODO: this element is added to workaround state issues widgets.push(space::horizontal().width(Length::Shrink).into()); @@ -710,6 +731,7 @@ impl ApplicationExt for App { widgets }); + let content_col = crate::widget::column::with_capacity(2) .push(content_row) .push_maybe(self.footer().map(|footer| { diff --git a/src/applet/mod.rs b/src/applet/mod.rs index ff376aab..f7fa5b62 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -392,7 +392,7 @@ impl Context { } }), ) - .width(Length::Shrink) + .width(Length::Fill) .height(Length::Shrink) .align_x(horizontal_align) .align_y(vertical_align), @@ -584,6 +584,7 @@ pub fn run(flags: App::Flags) -> iced::Result { BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, diff --git a/src/lib.rs b/src/lib.rs index 7e61730b..1a579f96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ pub use apply::{Also, Apply}; pub mod action; pub use action::Action; +pub mod anim; + #[cfg(feature = "winit")] pub mod app; #[cfg(feature = "winit")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 6a1e6060..937aabf9 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -107,7 +107,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -147,7 +147,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() @@ -193,7 +193,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 54e29786..a4e32378 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -357,7 +357,6 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); - let state = tree.state.downcast_mut::(); } fn update( diff --git a/src/widget/cards.rs b/src/widget/cards.rs new file mode 100644 index 00000000..b8e17636 --- /dev/null +++ b/src/widget/cards.rs @@ -0,0 +1,587 @@ +//! An expandable stack of cards +use std::time::Duration; + +use self::iced_core::{ + Element, Event, Length, Size, Vector, Widget, border::Radius, id::Id, layout::Node, + renderer::Quad, widget::Tree, +}; +use crate::{ + anim, + iced_core::{self, Border, Shadow}, + widget::{ + button, + card::style::Style, + column, + icon::{self, Handle}, + row, text, + }, +}; +use float_cmp::approx_eq; +use iced::widget; +use iced_core::{widget::tree, window}; + +const ICON_SIZE: u16 = 16; +const TOP_SPACING: u16 = 4; +const VERTICAL_SPACING: f32 = 8.0; +const PADDING: u16 = 16; +const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; +const BG_CARD_BORDER_RADIUS: f32 = 8.0; +const BG_CARD_MARGIN_STEP: f32 = 8.0; + +/// get an expandable stack of cards +#[allow(clippy::too_many_arguments)] +pub fn cards<'a, Message, F, G>( + id: widget::Id, + card_inner_elements: Vec>, + on_clear_all: Message, + on_show_more: Option, + on_activate: Option, + show_more_label: &'a str, + show_less_label: &'a str, + clear_all_label: &'a str, + show_less_icon: Option, + expanded: bool, +) -> Cards<'a, Message, crate::Renderer> +where + Message: 'static + Clone, + F: 'a + Fn(bool) -> Message, + G: 'a + Fn(usize) -> Message, +{ + Cards::new( + id, + card_inner_elements, + on_clear_all, + on_show_more, + on_activate, + show_more_label, + show_less_label, + clear_all_label, + show_less_icon, + expanded, + ) +} + +impl<'a, Message, Renderer> Cards<'a, Message, Renderer> +where + Renderer: iced_core::text::Renderer, +{ + fn fully_expanded(&self, t: f32) -> bool { + self.expanded && self.elements.len() > 1 && self.can_show_more && approx_eq!(f32, t, 1.0) + } + + fn fully_unexpanded(&self, t: f32) -> bool { + self.elements.len() == 1 + || (!self.expanded && (!self.can_show_more || approx_eq!(f32, t, 0.0))) + } +} + +/// An expandable stack of cards. +#[allow(missing_debug_implementations)] +pub struct Cards<'a, Message, Renderer = crate::Renderer> +where + Renderer: iced_core::text::Renderer, +{ + id: Id, + show_less_button: Element<'a, Message, crate::Theme, Renderer>, + clear_all_button: Element<'a, Message, crate::Theme, Renderer>, + elements: Vec>, + expanded: bool, + can_show_more: bool, + width: Length, + anim_multiplier: f32, + duration: Duration, +} + +impl<'a, Message> Cards<'a, Message, crate::Renderer> +where + Message: Clone + 'static, +{ + /// Get an expandable stack of cards + #[allow(clippy::too_many_arguments)] + pub fn new( + id: widget::Id, + card_inner_elements: Vec>, + on_clear_all: Message, + on_show_more: Option, + on_activate: Option, + show_more_label: &'a str, + show_less_label: &'a str, + clear_all_label: &'a str, + show_less_icon: Option, + expanded: bool, + ) -> Self + where + F: 'a + Fn(bool) -> Message, + G: 'a + Fn(usize) -> Message, + { + let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some(); + + Self { + can_show_more, + id: Id::unique(), + show_less_button: { + let mut show_less_children = Vec::with_capacity(3); + if let Some(source) = show_less_icon { + show_less_children.push(icon::icon(source).size(ICON_SIZE).into()); + } + show_less_children.push(text::body(show_less_label).width(Length::Shrink).into()); + show_less_children.push( + icon::from_name("pan-up-symbolic") + .size(ICON_SIZE) + .icon() + .into(), + ); + + let button_content = row::with_children(show_less_children) + .align_y(iced_core::Alignment::Center) + .spacing(TOP_SPACING) + .width(Length::Shrink); + + Element::from( + button::custom(button_content) + .class(crate::theme::Button::Text) + .width(Length::Shrink) + .on_press_maybe(on_show_more.as_ref().map(|f| f(false))) + .padding([PADDING / 2, PADDING]), + ) + }, + clear_all_button: Element::from( + button::custom(text(clear_all_label)) + .class(crate::theme::Button::Text) + .width(Length::Shrink) + .on_press(on_clear_all) + .padding([PADDING / 2, PADDING]), + ), + elements: card_inner_elements + .into_iter() + .enumerate() + .map(|(i, w)| { + let custom_content = if i == 0 && !expanded && can_show_more { + column::with_capacity(2) + .push(w) + .push(text::caption(show_more_label)) + .spacing(VERTICAL_SPACING) + .align_x(iced_core::Alignment::Center) + .into() + } else { + w + }; + + let b = crate::iced::widget::button(custom_content) + .class(crate::theme::iced::Button::Card) + .padding(PADDING); + if i == 0 && !expanded && can_show_more { + b.on_press_maybe(on_show_more.as_ref().map(|f| f(true))) + } else { + b.on_press_maybe(on_activate.as_ref().map(|f| f(i))) + } + .into() + }) + // we will set the width of the container to shrink, then when laying out the top bar + // we will set the fill limit to the max of the shrink top bar width and the max shrink width of the + // cards + .collect(), + width: Length::Shrink, + anim_multiplier: 1.0, + expanded, + duration: Duration::from_millis(200), + } + } + + /// Set the width of the cards stack + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + #[must_use] + /// The default animation time is 100ms, to speed up the toggle + /// animation use a value less than 1.0, and to slow down the + /// animation use a value greater than 1.0. + pub fn anim_multiplier(mut self, multiplier: f32) -> Self { + self.anim_multiplier = multiplier; + self + } + + pub fn duration(mut self, dur: Duration) -> Self { + self.duration = dur; + self + } + + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } +} + +impl<'a, Message, Renderer> Widget for Cards<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer, +{ + fn children(&self) -> Vec { + [&self.show_less_button, &self.clear_all_button] + .iter() + .map(|w| Tree::new(w.as_widget())) + .chain(self.elements.iter().map(|w| Tree::new(w.as_widget()))) + .collect() + } + + fn diff(&mut self, tree: &mut Tree) { + let mut children: Vec<_> = vec![ + self.show_less_button.as_widget_mut(), + self.clear_all_button.as_widget_mut(), + ] + .into_iter() + .chain( + self.elements + .iter_mut() + .map(iced_core::Element::as_widget_mut), + ) + .collect(); + + tree.diff_children(children.as_mut_slice()); + } + + #[allow(clippy::too_many_lines)] + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let my_state = tree.state.downcast_ref::(); + + let mut children = Vec::with_capacity(1 + self.elements.len()); + let mut size = Size::new(0.0, 0.0); + let tree_children = &mut tree.children; + let count = self.elements.len(); + if self.elements.is_empty() { + return Node::with_children(Size::new(1., 1.), children); + } + let s = anim::smootherstep(my_state.anim.t(self.duration, self.expanded)); + let fully_expanded: bool = self.fully_expanded(s); + let fully_unexpanded: bool = self.fully_unexpanded(s); + + let show_less = &mut self.show_less_button; + let clear_all = &mut self.clear_all_button; + + let show_less_node = if self.can_show_more { + show_less + .as_widget_mut() + .layout(&mut tree_children[0], renderer, limits) + } else { + Node::new(Size::default()) + }; + let clear_all_node = + clear_all + .as_widget_mut() + .layout(&mut tree_children[1], renderer, limits); + size.width += show_less_node.size().width + clear_all_node.size().width; + + let custom_limits = limits.min_width(size.width); + for (c, t) in self.elements.iter_mut().zip(tree_children[2..].iter_mut()) { + let card_node = c.as_widget_mut().layout(t, renderer, &custom_limits); + size.width = size.width.max(card_node.size().width); + } + + if fully_expanded { + let show_less = &mut self.show_less_button; + let clear_all = &mut self.clear_all_button; + + let show_less_node = if self.can_show_more { + show_less + .as_widget_mut() + .layout(&mut tree_children[0], renderer, limits) + } else { + Node::new(Size::default()) + }; + let clear_all_node = if self.can_show_more { + let mut n = + clear_all + .as_widget_mut() + .layout(&mut tree_children[1], renderer, limits); + let clear_all_node_size = n.size(); + n = clear_all_node + .translate(Vector::new(size.width - clear_all_node_size.width, 0.0)); + size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING; + n + } else { + Node::new(Size::default()) + }; + + children.push(show_less_node); + children.push(clear_all_node); + } + + let custom_limits = limits + .min_width(size.width) + .max_width(size.width) + .width(Length::Fixed(size.width)); + + for (i, (c, t)) in self + .elements + .iter_mut() + .zip(tree_children[2..].iter_mut()) + .enumerate() + { + let progress = s * size.height; + let card_node = c + .as_widget_mut() + .layout(t, renderer, &custom_limits) + .translate(Vector::new(0.0, progress)); + + size.height = size.height.max(progress + card_node.size().height); + + children.push(card_node); + + if fully_unexpanded { + let width = children.last().unwrap().bounds().width; + + // push the background card nodes + for i in 1..self.elements.len().min(3) { + // height must be 16px for 8px padding + // but we only want 4px visible + + let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; + let node = + Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) + .translate(Vector::new( + margin, + size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, + )); + size.height += BG_CARD_VISIBLE_HEIGHT; + children.push(node); + } + break; + } + + if i + 1 < count { + size.height += VERTICAL_SPACING; + } + } + + Node::with_children(size, children) + } + + fn draw( + &self, + state: &iced_core::widget::Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + viewport: &iced_core::Rectangle, + ) { + let my_state = state.state.downcast_ref::(); + + // there are 4 cases for drawing + // 1. empty entries list + // Nothing to draw + // 2. un-expanded + // go through the layout, draw the card, the inner card, and the bg cards + // 3. expanding / unexpanding + // go through the layout. draw each card and its inner card + // 4. expanded => + // go through the layout. draw the top bar, and do all of 3 + // cards may be hovered + // any buttons may have a hover state as well + if self.elements.is_empty() { + return; + } + + let t = my_state.anim.t(self.duration, self.expanded); + let fully_unexpanded = self.fully_unexpanded(t); + let fully_expanded = self.fully_expanded(t); + + let mut layout = layout.children(); + let mut tree_children = state.children.iter(); + + if fully_expanded { + let show_less = &self.show_less_button; + let clear_all = &self.clear_all_button; + + let show_less_layout = layout.next().unwrap(); + let clear_all_layout = layout.next().unwrap(); + + show_less.as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + show_less_layout, + cursor, + viewport, + ); + + clear_all.as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + clear_all_layout, + cursor, + viewport, + ); + } else { + _ = tree_children.next(); + _ = tree_children.next(); + } + + // Draw first to appear behind + if fully_unexpanded { + let card_layout = layout.next().unwrap(); + let appearance = Style::default(); + let bg_layout = layout.collect::>(); + for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border: Border { + radius: Radius::from([ + 0.0, + 0.0, + BG_CARD_BORDER_RADIUS, + BG_CARD_BORDER_RADIUS, + ]), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }, + if i == 0 { + appearance.card_1 + } else { + appearance.card_2 + }, + ); + } + self.elements[0].as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + card_layout, + cursor, + viewport, + ); + } else { + let layout = layout.collect::>(); + // draw in reverse order so later cards appear behind earlier cards + for ((inner, layout), c_state) in self + .elements + .iter() + .rev() + .zip(layout.into_iter().rev()) + .zip(tree_children.rev()) + { + inner + .as_widget() + .draw(c_state, renderer, theme, style, layout, cursor, viewport); + } + } + } + + fn update( + &mut self, + state: &mut Tree, + event: &iced_core::Event, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn iced_core::Clipboard, + shell: &mut iced_core::Shell<'_, Message>, + viewport: &iced_core::Rectangle, + ) { + if self.elements.is_empty() { + return; + } + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + let state = state.state.downcast_mut::(); + + state.anim.anim_done(self.duration); + if state.anim.last_change.is_some() { + shell.request_redraw(); + shell.invalidate_layout(); + } + } + + let my_state = state.state.downcast_ref::(); + + let mut layout = layout.children(); + let mut tree_children = state.children.iter_mut(); + let t = my_state.anim.t(self.duration, self.expanded); + let fully_expanded = self.fully_expanded(t); + let fully_unexpanded = self.fully_unexpanded(t); + let show_less_state = tree_children.next(); + let clear_all_state = tree_children.next(); + + if fully_expanded { + let c_layout = layout.next().unwrap(); + let state = show_less_state.unwrap(); + self.show_less_button.as_widget_mut().update( + state, event, c_layout, cursor, renderer, clipboard, shell, viewport, + ); + + if shell.is_event_captured() { + return; + } + + let c_layout = layout.next().unwrap(); + let state = clear_all_state.unwrap(); + self.clear_all_button.as_widget_mut().update( + state, &event, c_layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + if shell.is_event_captured() { + return; + } + + for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) { + inner.as_widget_mut().update( + c_state, &event, layout, cursor, renderer, clipboard, shell, viewport, + ); + if shell.is_event_captured() || fully_unexpanded { + break; + } + } + } + + fn size(&self) -> Size { + Size::new(self.width, Length::Shrink) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message> From> for Element<'a, Message, crate::Theme, crate::Renderer> +where + Message: Clone + 'a, +{ + fn from(cards: Cards<'a, Message>) -> Self { + Self::new(cards) + } +} + +#[derive(Debug, Default)] +pub struct State { + anim: anim::State, +} diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 008660a7..143a78b8 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -249,7 +249,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(self.content.as_widget_mut()); + tree.diff_children(std::slice::from_mut(&mut self.content)); let state = tree.state.downcast_mut::(); state.menu_bar_state.inner.with_data_mut(|inner| { menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 9faa2605..b0a23fad 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -291,7 +291,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(self.container.as_widget_mut()); + tree.diff_children(std::slice::from_mut(&mut self.container)); } fn state(&self) -> iced_core::widget::tree::State { @@ -337,7 +337,7 @@ impl Widget shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - let s = self.container.as_widget_mut().update( + self.container.as_widget_mut().update( &mut tree.children[0], event, layout, diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index c8627482..07b448a5 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -131,21 +131,25 @@ impl< ); } + #[must_use] pub fn on_start(mut self, on_start: Option) -> Self { self.on_start = on_start; self } + #[must_use] pub fn on_cancel(mut self, on_cancelled: Option) -> Self { self.on_cancelled = on_cancelled; self } + #[must_use] pub fn on_finish(mut self, on_finish: Option) -> Self { self.on_finish = on_finish; self } + #[must_use] pub fn window(mut self, window: window::Id) -> Self { self.window = Some(window); self @@ -164,7 +168,7 @@ impl iced_core::widget::tree::State { @@ -197,19 +201,15 @@ impl, viewport: &Rectangle, ) { - let ret = self.container.as_widget_mut().update( + self.container.as_widget_mut().update( &mut tree.children[0], - &event, + event, layout, cursor, renderer, @@ -241,12 +241,11 @@ impl { if let Some(position) = cursor.position() { if !state.hovered { - return ret; + return; } state.left_pressed_position = Some(position); shell.capture_event(); - return; } } mouse::Event::ButtonReleased(mouse::Button::Left) @@ -254,7 +253,6 @@ impl { if let Some(position) = cursor.position() { @@ -262,7 +260,7 @@ impl self.drag_threshold { @@ -281,16 +279,15 @@ impl return ret, + _ => (), }, Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { if state.is_dragging { @@ -301,7 +298,6 @@ impl { if state.is_dragging { @@ -312,11 +308,9 @@ impl return ret, + _ => (), } - ret } fn mouse_interaction( diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 695c8405..1465a9d7 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,7 +5,7 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced::Length; +use iced::{Length, mouse}; use iced_core::{Vector, Widget, widget::tree}; use std::{borrow::Cow, cmp}; @@ -206,6 +206,7 @@ impl Widget ) { let child_state = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget_mut().update( child_state, event, @@ -215,7 +216,7 @@ impl Widget clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -435,6 +436,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } + if let Some(message) = self.on_double_click.clone() { widget = widget.on_double_press(message); } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index c8e49e04..716ee138 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -57,7 +57,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -88,7 +88,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() @@ -124,7 +124,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 49df998a..136b49ea 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -112,6 +112,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { crate::widget::column::with_children(self.children) .spacing(self.spacing) .padding(self.padding) + .width(iced::Length::Fill) .apply(container) .padding([self.spacing, 0]) .class(self.style) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 05fcc133..9d4b09b0 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -580,7 +580,7 @@ where &mut self.menu_roots, view_cursor, tree, - &event, + event, layout, renderer, clipboard, @@ -609,6 +609,13 @@ where }); match event { + Mouse(mouse::Event::ButtonPressed(Left)) + | Touch(touch::Event::FingerPressed { .. }) + if view_cursor.is_over(layout.bounds()) => + { + // TODO should we track that it has been pressed? + shell.capture_event(); + } Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { let create_popup = my_state.inner.with_data_mut(|state| { let mut create_popup = false; @@ -627,6 +634,7 @@ where ))] { let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.capture_event(); shell.publish(surface_action(crate::surface::action::destroy_popup( _id, @@ -640,6 +648,7 @@ where if !create_popup { return; } + shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", @@ -653,6 +662,7 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { + shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index d52c929d..4f97d30e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1008,7 +1008,7 @@ impl Widget( menu_bounds, }; state.menu_states.push(ms); - // Hack to ensure menu opens properly shell.invalidate_layout(); diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 30b75a10..f63cdc37 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -127,6 +127,10 @@ pub use color_picker::{ColorPicker, ColorPickerModel}; #[doc(inline)] pub use iced::widget::qr_code; +mod cards; +#[doc(inline)] +pub use cards::cards; + pub mod context_drawer; #[doc(inline)] pub use context_drawer::{ContextDrawer, context_drawer}; diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 951b3757..7a82cd86 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -127,7 +127,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let tree = content_tree_mut(tree); + let tree = &mut tree.children[0]; self.content.as_widget_mut().layout(tree, renderer, limits) } @@ -138,19 +138,9 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - tree, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); + self.content + .as_widget_mut() + .operate(content_tree_mut(tree), layout, renderer, operation); } fn update( @@ -183,7 +173,7 @@ where } self.content.as_widget_mut().update( - content_tree_mut(tree), + &mut tree.children[0], event, layout, cursor_position, @@ -265,7 +255,6 @@ where overlay_position.y = overlay_position.y.round(); translation.x += overlay_position.x; translation.y += overlay_position.y; - Some(overlay::Element::new(Box::new(Overlay { tree: &mut tree.children[1], content: popup, @@ -275,7 +264,7 @@ where }))) } else { self.content.as_widget_mut().overlay( - content_tree_mut(tree), + &mut tree.children[0], layout, renderer, viewport, diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 831e9460..338c0a4e 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -165,7 +165,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.label); + tree.diff_children(std::slice::from_mut(&mut self.label)); } fn size(&self) -> Size { Size { diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 0c7fbad3..3bb44276 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -81,7 +81,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -131,7 +131,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 162d1d21..f6de999e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2047,6 +2047,9 @@ where bounds.y = center_y; if self.model.text(key).is_some_and(|text| !text.is_empty()) { + // FIXME why has this behavior changed? Does the center alignment not work with infinite bounds now? + bounds.y -= state.paragraphs[key].min_height() / 2.; + // Draw the text for this segmented button or tab. renderer.fill_paragraph( state.paragraphs[key].raw(), @@ -2055,7 +2058,9 @@ where Rectangle { x: bounds.x, width: bounds.width, - ..original_bounds + height: original_bounds.height, + y: bounds.y, + // ..original_bounds, }, ); } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 65179d99..fafc6d70 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,17 +1,418 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 +//! Show toggle controls using togglers. -use iced::{Length, widget}; -use iced_core::text; +use std::time::{Duration, Instant}; -pub fn toggler<'a, Message, Theme: iced_widget::toggler::Catalog, Renderer>( - is_checked: bool, -) -> widget::Toggler<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer + text::Renderer, -{ - widget::Toggler::new(is_checked) - .size(24) - .spacing(0) - .width(Length::Shrink) +use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status}; +use iced_core::{ + Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, + layout, mouse, + renderer::{self, Renderer}, + text, + widget::{self, Tree, tree}, + window, +}; +use iced_widget::Id; + +pub use crate::iced_widget::toggler::{Catalog, Style}; + +pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { + Toggler::new(is_checked) +} +/// A toggler widget. +#[allow(missing_debug_implementations)] +pub struct Toggler<'a, Message> { + id: Id, + is_toggled: bool, + on_toggle: Option Message + 'a>>, + label: Option, + width: Length, + size: f32, + text_size: Option, + text_line_height: text::LineHeight, + text_alignment: text::Alignment, + text_shaping: text::Shaping, + spacing: f32, + font: Option, + duration: Duration, +} + +impl<'a, Message> Toggler<'a, Message> { + /// The default size of a [`Toggler`]. + pub const DEFAULT_SIZE: f32 = 24.0; + + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + pub fn new(is_toggled: bool) -> Self { + Toggler { + id: Id::unique(), + is_toggled, + on_toggle: None, + label: None, + width: Length::Fill, + size: Self::DEFAULT_SIZE, + text_size: None, + text_line_height: text::LineHeight::default(), + text_alignment: text::Alignment::Left, + text_shaping: text::Shaping::Advanced, + spacing: 0.0, + font: None, + duration: Duration::from_millis(200), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into().0; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: impl Into) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Toggler`]. + pub fn text_line_height(mut self, line_height: impl Into) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: text::Alignment) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Toggler`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: impl Into) -> Self { + self.spacing = spacing.into().0; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + /// + /// [`Font`]: cosmic::iced::text::Renderer::Font + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + pub fn duration(mut self, dur: Duration) -> Self { + self.duration = dur; + self + } + + pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self { + self.on_toggle = Some(Box::new(on_toggle)); + self + } + + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into(); + self + } +} + +impl<'a, Message> Widget for Toggler<'a, Message> { + fn size(&self) -> Size { + Size::new(self.width, Length::Shrink) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width); + + let res = next_to_each_other( + &limits, + self.spacing, + |limits| { + if let Some(label) = self.label.as_deref() { + let state = tree.state.downcast_mut::(); + let node = iced_core::widget::text::layout( + &mut state.text, + renderer, + limits, + label, + widget::text::Format { + width: self.width, + height: Length::Shrink, + line_height: self.text_line_height, + size: self.text_size.map(iced::Pixels), + font: self.font, + align_x: self.text_alignment, + align_y: alignment::Vertical::Top, + shaping: self.text_shaping, + wrapping: crate::iced_core::text::Wrapping::default(), + }, + ); + match self.width { + Length::Fill => { + let size = node.size(); + layout::Node::with_children( + Size::new(limits.width(Length::Fill).max().width, size.height), + vec![node], + ) + } + _ => node, + } + } else { + layout::Node::new(iced_core::Size::ZERO) + } + }, + |_| layout::Node::new(Size::new(48., 24.)), + ); + res + } + + fn update( + &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, + ) { + let Some(on_toggle) = self.on_toggle.as_ref() else { + return; + }; + let state = tree.state.downcast_mut::(); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = cursor_position.is_over(layout.bounds()); + + if mouse_over { + shell.publish((on_toggle)(!self.is_toggled)); + state.anim.changed(self.duration); + shell.capture_event(); + } + } + Event::Window(window::Event::RedrawRequested(now)) => { + state.anim.anim_done(self.duration); + if state.anim.last_change.is_some() { + shell.request_redraw(); + } + } + _ => {} + } + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor_position.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + + let mut children = layout.children(); + let label_layout = children.next().unwrap(); + + if let Some(_label) = &self.label { + let state: &State = tree.state.downcast_ref(); + iced_widget::text::draw( + renderer, + style, + label_layout.bounds(), + state.text.raw(), + iced_widget::text::Style::default(), + viewport, + ); + } + + let toggler_layout = children.next().unwrap(); + let bounds = toggler_layout.bounds(); + + let is_mouse_over = cursor_position.is_over(bounds); + + // let style = blend_appearances( + // theme.style( + // &(), + // if is_mouse_over { + // Status::Hovered { is_toggled: false } + // } else { + // Status::Active { is_toggled: false } + // }, + // ), + // theme.style( + // &(), + // if is_mouse_over { + // Status::Hovered { is_toggled: true } + // } else { + // Status::Active { is_toggled: true } + // }, + // ), + // percent, + // ); + + let style = theme.style( + &(), + if is_mouse_over { + Status::Hovered { + is_toggled: self.is_toggled, + } + } else { + Status::Active { + is_toggled: self.is_toggled, + } + }, + ); + + let space = style.handle_margin; + + let toggler_background_bounds = Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_background_bounds, + border: Border { + radius: style.border_radius, + ..Default::default() + }, + ..renderer::Quad::default() + }, + style.background, + ); + let mut t = state.anim.t(self.duration, self.is_toggled); + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + anim::slerp( + space, + bounds.width - space - (bounds.height - (2.0 * space)), + t, + ), + + y: bounds.y + space, + width: bounds.height - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_foreground_bounds, + border: Border { + radius: style.handle_radius, + ..Default::default() + }, + ..renderer::Quad::default() + }, + style.foreground, + ); + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> { + Element::new(toggler) + } +} + +/// Produces a [`Node`] with two children nodes one right next to each other. +pub fn next_to_each_other( + limits: &iced::Limits, + spacing: f32, + left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, + right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, +) -> iced_core::layout::Node { + let mut right_node = right(limits); + let right_size = right_node.size(); + + let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0)); + let mut left_node = left(&left_limits); + let left_size = left_node.size(); + + let (left_y, right_y) = if left_size.height > right_size.height { + (0.0, (left_size.height - right_size.height) / 2.0) + } else { + ((right_size.height - left_size.height) / 2.0, 0.0) + }; + + left_node = left_node.move_to(iced::Point::new(0.0, left_y)); + right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y)); + + iced_core::layout::Node::with_children( + Size::new( + left_size.width + spacing + right_size.width, + left_size.height.max(right_size.height), + ), + vec![left_node, right_node], + ) +} + +#[derive(Debug, Default)] +pub struct State { + text: widget::text::State<::Paragraph>, + anim: anim::State, } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index ceb234a9..b16720cd 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -240,7 +240,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() From e6fe1a68115fe8d62766cc6665c52f7bb4e4c340 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Feb 2026 22:54:08 -0500 Subject: [PATCH 055/168] fix: ellipsize --- src/widget/toggler.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index fafc6d70..312b50d5 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -34,6 +34,7 @@ pub struct Toggler<'a, Message> { spacing: f32, font: Option, duration: Duration, + ellipsize: text::Ellipsize, } impl<'a, Message> Toggler<'a, Message> { @@ -63,6 +64,7 @@ impl<'a, Message> Toggler<'a, Message> { spacing: 0.0, font: None, duration: Duration::from_millis(200), + ellipsize: text::Ellipsize::None, } } @@ -108,6 +110,12 @@ impl<'a, Message> Toggler<'a, Message> { self } + /// Sets the [`text::Ellipsize`] strategy of the [`Toggler`]. + pub fn ellipsize(mut self, ellipsize: text::Ellipsize) -> Self { + self.ellipsize = ellipsize; + self + } + /// Sets the [`Font`] of the text of the [`Toggler`] /// /// [`Font`]: cosmic::iced::text::Renderer::Font @@ -188,6 +196,7 @@ impl<'a, Message> Widget for Toggler<'a, align_y: alignment::Vertical::Top, shaping: self.text_shaping, wrapping: crate::iced_core::text::Wrapping::default(), + ellipsize: self.ellipsize, }, ); match self.width { From 0d37dc69e3fae08acc14a91f6e491d7a6c5feaf6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 20:47:15 -0500 Subject: [PATCH 056/168] fix: applet popup width --- src/applet/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/applet/mod.rs b/src/applet/mod.rs index f7fa5b62..0cbcacab 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -392,7 +392,6 @@ impl Context { } }), ) - .width(Length::Fill) .height(Length::Shrink) .align_x(horizontal_align) .align_y(vertical_align), From 71e2c7c99eafd20cd2f11a0b8a3c4fb7d2c9eda5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 20:48:11 -0500 Subject: [PATCH 057/168] fix: responsive menu layout --- examples/application/Cargo.toml | 5 ++-- src/applet/mod.rs | 2 ++ src/widget/responsive_container.rs | 44 +++++++++++++++++++++------- src/widget/toggler.rs | 5 ++++ src/widget/wayland/tooltip/widget.rs | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index b1ac1242..f4b62cdb 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,8 +11,9 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" -branch = "iced-rebase" +# git = "https://github.com/pop-os/libcosmic" +# branch = "iced-rebase" +path = "../.." features = [ "debug", "winit", diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0cbcacab..e18c9aad 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -234,6 +234,7 @@ impl Context { }) .width(Length::Fixed(suggested.0 as f32)) .height(Length::Fixed(suggested.1 as f32)); + dbg!(suggested); self.button_from_element(icon, symbolic) } @@ -250,6 +251,7 @@ impl Context { (applet_padding_minor_axis, applet_padding_major_axis) }; + dbg!(suggested.0 + 2 * horizontal_padding); crate::widget::button::custom(layer_container(content).center(Length::Fill)) .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32)) .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32)) diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 3bb44276..b9b6a289 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -95,7 +95,7 @@ where limits: &layout::Limits, ) -> layout::Node { let state = tree.state.downcast_mut::(); - let unrestricted_size = self.size.unwrap_or_else(|| { + let mut unrestricted_size = self.size.unwrap_or_else(|| { let node = self.content .as_widget_mut() @@ -103,21 +103,45 @@ where node.size() }); - let max_size = limits.max(); - let old_max = state.limits.max(); - state.needs_update = (unrestricted_size.width > max_size.width) - ^ (state.size.width > old_max.width) - || (unrestricted_size.height > max_size.height) ^ (state.size.height > old_max.height); - if state.needs_update { - state.limits = *limits; - state.size = unrestricted_size; - } + let cur_unrestricted_size = { + let node = + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, &Limits::NONE); + node.size() + }; + let max_size = limits.max(); + + let old_max = state.limits.max(); + + state.needs_update = (cur_unrestricted_size.width > max_size.width) + || (cur_unrestricted_size.width > old_max.width) + || (cur_unrestricted_size.height > max_size.height) + || (cur_unrestricted_size.height > old_max.height) + || ((unrestricted_size.width <= max_size.width) + && (unrestricted_size.height <= max_size.height) + && (unrestricted_size.width - cur_unrestricted_size.width > 1. + || unrestricted_size.height - cur_unrestricted_size.height > 1.)); + + if unrestricted_size.width < cur_unrestricted_size.width { + state.needs_update = true; + unrestricted_size.width = cur_unrestricted_size.width; + } else if unrestricted_size.height < cur_unrestricted_size.height { + state.needs_update = true; + unrestricted_size.height = cur_unrestricted_size.height; + } let node = self .content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); + + if state.needs_update { + state.limits = *limits; + state.size = unrestricted_size; + } + layout::Node::with_children(size, vec![node]) } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 312b50d5..2cd6a785 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -139,6 +139,11 @@ impl<'a, Message> Toggler<'a, Message> { self } + pub fn on_toggle_maybe(mut self, on_toggle: Option Message + 'a>) -> Self { + self.on_toggle = on_toggle.map(|t| Box::new(t) as _); + self + } + /// Sets the label of the [`Button`]. pub fn label(mut self, label: impl Into>) -> Self { self.label = label.into(); diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index b16720cd..7bf0991a 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -263,7 +263,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - let status = update( + update( self.id.clone(), event.clone(), layout, From 7554540b78e093ad1f12f293535c903f3889c350 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 21:57:15 -0500 Subject: [PATCH 058/168] fix: update for applet widgets and grid --- src/applet/column.rs | 61 ++++++++++++++++++++------------------- src/applet/row.rs | 61 ++++++++++++++++++++------------------- src/widget/grid/widget.rs | 27 ++++++++--------- 3 files changed, 76 insertions(+), 73 deletions(-) diff --git a/src/applet/column.rs b/src/applet/column.rs index 8b3c68e9..9657b566 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -320,38 +320,25 @@ where } } - self.children + for (((i, child), state), c_layout) in self + .children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - .map(|(((i, child), state), c_layout)| { - let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().update( + { + let mut cursor_virtual = cursor; + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + return child.as_widget_mut().update( state, &event, c_layout.with_virtual_offset(layout.virtual_offset()), @@ -360,8 +347,22 @@ where clipboard, shell, viewport, - ) - }); + ); + } else if my_state.hovered.is_some_and(|h| i != h) { + cursor_virtual = mouse::Cursor::Unavailable; + } + + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/src/applet/row.rs b/src/applet/row.rs index 2a770503..a6745d1c 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -309,39 +309,26 @@ where } } - self.children + for (((i, child), state), c_layout) in self + .children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - .map(|(((i, child), state), c_layout)| { - let mut cursor_virtual = cursor; + { + let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().update( + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + return child.as_widget_mut().update( state, &event, c_layout.with_virtual_offset(layout.virtual_offset()), @@ -350,8 +337,22 @@ where clipboard, shell, viewport, - ) - }); + ); + } else if my_state.hovered.is_some_and(|h| i != h) { + cursor_virtual = mouse::Cursor::Unavailable; + } + + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index f88dfc2a..e59ba90d 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -189,22 +189,23 @@ impl Widget for Grid< shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.children + for ((child, state), c_layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); + { + child.as_widget_mut().update( + state, + event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( From 89ee66f25113dac0f8f06e734c6453323b508297 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 22:47:28 -0500 Subject: [PATCH 059/168] fix: menu bar and flex row event handling --- src/widget/flex_row/widget.rs | 27 ++++++++++++++------------- src/widget/menu/menu_bar.rs | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index f7b90f66..b891c170 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -160,22 +160,23 @@ impl Widget for FlexR shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.children + for ((child, state), c_layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); + { + child.as_widget_mut().update( + state, + event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 9d4b09b0..7007befb 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -810,21 +810,21 @@ fn process_root_events( shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - menu_roots + for ((root, t), lo) in menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((root, t), lo)| { - // assert!(t.tag == tree::Tag::stateless()); - root.item.update( - &mut t.children[root.index], - event, - lo, - view_cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); + { + // assert!(t.tag == tree::Tag::stateless()); + root.item.update( + &mut t.children[root.index], + event, + lo, + view_cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } From fb1a7d36407d863c80ee4be1e718969451697f58 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 22:48:07 -0500 Subject: [PATCH 060/168] fix: open-dialog example --- examples/open-dialog/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 10e46315..29061534 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -207,7 +207,7 @@ impl cosmic::Application for App { ); content.push( - iced::widget::vertical_space() + iced::widget::space::vertical() .height(Length::Fixed(12.0)) .into(), ); From 442ce6ad0c3c74d878d4a2d3ff467daa63275f11 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 23:32:38 -0500 Subject: [PATCH 061/168] fix: context-menu when a popup is created and a focus event is received, we shouldn't close the popups, because it may be a focus event for a popup --- src/widget/context_menu.rs | 63 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 143a78b8..25953639 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -13,7 +13,7 @@ use derive_setters::Setters; use iced::touch::Finger; use iced::{Event, Vector, keyboard, window}; use iced_core::widget::{Tree, Widget, tree}; -use iced_core::{Length, Point, Size, event, mouse, touch}; +use iced_core::{Length, Point, Size, mouse, touch}; use std::collections::HashSet; use std::sync::Arc; @@ -85,6 +85,7 @@ impl ContextMenu<'_, Message> { // close existing popups state.menu_states.clear(); state.active_root.clear(); + dbg!("closing existing popups"); shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); state.view_cursor = view_cursor; ( @@ -336,13 +337,12 @@ impl Widget .with_data(|d| !d.open && !d.active_root.is_empty()); let open = state.menu_bar_state.inner.with_data_mut(|state| { - if reset { - if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - if let Some(handler) = self.on_surface_action.as_ref() { - shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); - state.reset(); - } - } + if reset + && let Some(popup_id) = state.popup_id.get(&self.window_id).copied() + && let Some(handler) = self.on_surface_action.as_ref() + { + shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); + state.reset(); } state.open }); @@ -356,7 +356,6 @@ impl Widget mouse::Button::Right | mouse::Button::Left, )) | Event::Touch(touch::Event::FingerPressed { .. }) - | Event::Window(window::Event::Focused) if open ) { state.menu_bar_state.inner.with_data_mut(|state| { @@ -366,15 +365,14 @@ impl Widget state.open = false; #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(id) = state.popup_id.remove(&self.window_id) { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell - .publish(surface_action(crate::surface::action::destroy_popup(id))); - } - state.view_cursor = cursor; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(id) = state.popup_id.remove(&self.window_id) + { + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.publish(surface_action(crate::surface::action::destroy_popup(id))); } + state.view_cursor = cursor; } }); } @@ -388,7 +386,7 @@ impl Widget } Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(&id); + state.fingers_pressed.remove(id); } _ => (), @@ -397,7 +395,7 @@ impl Widget // Present a context menu on a right click event. if !was_open && self.context_menu.is_some() - && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) + && (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); let state = tree.state.downcast_mut::(); @@ -412,9 +410,9 @@ impl Widget shell.capture_event(); return; - } else if !was_open && right_button_released(&event) - || (touch_lifted(&event)) - || left_button_released(&event) + } else if !was_open && right_button_released(event) + || (touch_lifted(event)) + || left_button_released(event) { state.menu_bar_state.inner.with_data_mut(|state| { was_open = true; @@ -427,16 +425,15 @@ impl Widget feature = "winit", feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(id) = state.popup_id.remove(&self.window_id) { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.publish(surface_action( - crate::surface::action::destroy_popup(id), - )); - } - state.view_cursor = cursor; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(id) = state.popup_id.remove(&self.window_id) + { + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell + .publish(surface_action(crate::surface::action::destroy_popup(id))); } + state.view_cursor = cursor; } }); } @@ -450,7 +447,7 @@ impl Widget clipboard, shell, viewport, - ) + ); } fn overlay<'b>( @@ -458,7 +455,7 @@ impl Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, - viewport: &iced::Rectangle, + _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] From bee2d591db0428d7fe516ed9caddcee728a27b31 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 23 Feb 2026 14:50:52 -0500 Subject: [PATCH 062/168] chore: update iced --- examples/application/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index f4b62cdb..b1ac1242 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,9 +11,8 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -# git = "https://github.com/pop-os/libcosmic" -# branch = "iced-rebase" -path = "../.." +git = "https://github.com/pop-os/libcosmic" +branch = "iced-rebase" features = [ "debug", "winit", From 904133397b1de0db13b20ad6454d34a7e82fb61f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 11:10:34 -0500 Subject: [PATCH 063/168] fix: toggler width fixes & cleanup --- examples/applet/src/window.rs | 3 +-- src/applet/mod.rs | 2 -- src/widget/context_menu.rs | 2 +- src/widget/mod.rs | 2 +- src/widget/settings/item.rs | 10 ++++++++-- src/widget/toggler.rs | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 547863f2..4e05c70a 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -128,8 +128,7 @@ impl cosmic::Application for Window { "Example row", cosmic::widget::container( toggler(state.example_row) - .on_toggle(Message::ToggleExampleRow) - .width(Length::Fill), + .on_toggle(Message::ToggleExampleRow), ), )) .add(popup_dropdown( diff --git a/src/applet/mod.rs b/src/applet/mod.rs index e18c9aad..0cbcacab 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -234,7 +234,6 @@ impl Context { }) .width(Length::Fixed(suggested.0 as f32)) .height(Length::Fixed(suggested.1 as f32)); - dbg!(suggested); self.button_from_element(icon, symbolic) } @@ -251,7 +250,6 @@ impl Context { (applet_padding_minor_axis, applet_padding_major_axis) }; - dbg!(suggested.0 + 2 * horizontal_padding); crate::widget::button::custom(layer_container(content).center(Length::Fill)) .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32)) .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32)) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 25953639..200021c3 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -85,7 +85,7 @@ impl ContextMenu<'_, Message> { // close existing popups state.menu_states.clear(); state.active_root.clear(); - dbg!("closing existing popups"); + shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); state.view_cursor = view_cursor; ( diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f63cdc37..eae255bc 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -350,7 +350,7 @@ pub use toaster::{Toast, ToastId, Toasts, toaster}; mod toggler; #[doc(inline)] -pub use toggler::toggler; +pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index a17f2071..110ab7b7 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -41,6 +41,7 @@ pub fn item_row(children: Vec>) -> Row { row::with_children(children) .spacing(theme::spacing().space_xs) .align_y(iced::Alignment::Center) + .width(Length::Fill) } /// A settings item aligned in a flex row @@ -59,8 +60,9 @@ pub fn flex_item<'a, Message: 'static>( .wrapping(Wrapping::Word) .width(Length::Fill) .into(), - container(widget).into(), + container(widget).width(Length::Shrink).into(), ]) + .width(Length::Fill) } inner(title.into(), widget.into()) @@ -141,6 +143,10 @@ impl<'a, Message: 'static> Item<'a, Message> { is_checked: bool, message: impl Fn(bool) -> Message + 'static, ) -> Row<'a, Message> { - self.control(crate::widget::toggler(is_checked).on_toggle(message)) + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ) } } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2cd6a785..12bb8950 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -55,7 +55,7 @@ impl<'a, Message> Toggler<'a, Message> { is_toggled, on_toggle: None, label: None, - width: Length::Fill, + width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, text_line_height: text::LineHeight::default(), From 0298487096e03abc6bc31eec3e8d1339530f2714 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 13:26:38 -0500 Subject: [PATCH 064/168] fix: overlay event handling and mouse interaction --- src/app/mod.rs | 1 - src/widget/context_drawer/overlay.rs | 30 +++++++++++++++++++++++----- src/widget/context_drawer/widget.rs | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index abda71c1..e11ed7ae 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -12,7 +12,6 @@ use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; use iced::application::BootFn; -use iced_core::Widget; pub mod cosmic; pub mod settings; diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index eef9183b..39b34217 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -8,7 +8,7 @@ use iced::advanced::widget::{self, Operation}; use iced::advanced::{Clipboard, Shell}; use iced::advanced::{overlay, renderer}; use iced::{Event, Point, Size, mouse}; -use iced_core::Renderer; +use iced_core::{Renderer, touch}; pub(super) struct Overlay<'a, 'b, Message> { pub(crate) position: Point, @@ -65,7 +65,20 @@ where clipboard, shell, &layout.bounds(), - ) + ); + match event { + Event::Mouse(e) if !matches!(e, mouse::Event::CursorLeft) => { + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + } + Event::Touch(e) if !matches!(e, touch::Event::FingerLost { .. }) => { + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + } + _ => {} + } } fn draw( @@ -86,7 +99,7 @@ where cursor, &layout.bounds(), ); - }) + }); } fn operate( @@ -108,9 +121,16 @@ where ) -> mouse::Interaction { // TODO how to handle viewport here? let viewport = &layout.bounds(); - self.content + let interaction = self + .content .as_widget() - .mouse_interaction(self.tree, layout, cursor, viewport, renderer) + .mouse_interaction(self.tree, layout, cursor, viewport, renderer); + if let mouse::Interaction::None = interaction + && cursor.is_over(layout.bounds()) + { + return mouse::Interaction::Idle; + } + interaction } fn overlay<'c>( diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index e7ca5dab..7420738c 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -238,7 +238,7 @@ impl Widget for ContextDrawer<' clipboard, shell, viewport, - ) + ); } fn mouse_interaction( From 3d8596287c34f6151ab591acc23d30b971f7d805 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 15:26:08 -0500 Subject: [PATCH 065/168] fix: missed event status after rebase --- src/widget/dnd_destination.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index b0a23fad..a77101b9 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -531,7 +531,8 @@ impl Widget && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { shell.publish(f(s)); - return event::Status::Captured; + shell.capture_event(); + return; } if let (Some(msg), ret) = state.on_data_received( From 89d31e988da8cf4aa1737767d7e41e6476234277 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 17:47:58 -0500 Subject: [PATCH 066/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 73369a18..59fbf68c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 73369a18eb4069f3f3d1916fd1e17537ee87a587 +Subproject commit 59fbf68c541758197204aa52ceca9f89d63d1611 From 0e1a9d46eb09ed2c752ad6c6467f2d3437cd25ca Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Feb 2026 17:24:18 -0500 Subject: [PATCH 067/168] chore: update iced & cleanup text input --- iced | 2 +- src/widget/text_input/input.rs | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/iced b/iced index 59fbf68c..f7dc1803 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 59fbf68c541758197204aa52ceca9f89d63d1611 +Subproject commit f7dc18037113719633f450e549d9a6428b5c84b9 diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 5b6a53f3..3960cee1 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -651,11 +651,11 @@ where // 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() { - if let cursor::State::Index(index) = state.cursor.state(&old_value) { - if index == old_value.len() { - state.cursor.move_to(self.value.len()); - } + if state.is_focused() + && let cursor::State::Index(index) = state.cursor.state(&old_value) + { + if index == old_value.len() { + state.cursor.move_to(self.value.len()); } } @@ -935,7 +935,8 @@ where layout, self.manage_value, self.drag_threshold, - ) + self.always_active, + ); } #[inline] @@ -1358,6 +1359,7 @@ pub fn update<'a, Message: Clone + 'static>( layout: Layout<'_>, manage_value: bool, drag_threshold: f32, + always_active: bool, ) { let update_cache = |state, value| { replace_paragraph( @@ -1962,7 +1964,11 @@ pub fn update<'a, Message: Clone + 'static>( let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - + shell.request_redraw_at(window::RedrawRequest::At( + now.checked_add(Duration::from_millis(millis_until_redraw as u64)) + .unwrap_or(*now), + )); + } else if always_active { shell.request_redraw(); } } @@ -2340,11 +2346,9 @@ pub fn draw<'a, Message>( 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; + .is_multiple_of(2); if is_cursor_visible { if dnd_icon { (None, 0.0) @@ -2479,7 +2483,7 @@ pub fn draw<'a, Message>( }, bounds.position(), color, - *viewport, + text_bounds, ); }; From 925cc9a39f36e09e71e29da37cfeea841cb258b4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Mar 2026 10:35:53 -0500 Subject: [PATCH 068/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f7dc1803..0df654c1 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f7dc18037113719633f450e549d9a6428b5c84b9 +Subproject commit 0df654c14aa811e01362275a22201a9b9eff9ae3 From 5432fee1120155248c8689f33a462f2751519060 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Mar 2026 10:56:14 -0500 Subject: [PATCH 069/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 0df654c1..b479f3e8 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 0df654c14aa811e01362275a22201a9b9eff9ae3 +Subproject commit b479f3e87fd54b9e80a95cf1f4d7767f9dcfbccf From 0bfda2e28cc406411368600e112c2a813125ef2c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Mar 2026 13:36:07 -0500 Subject: [PATCH 070/168] chore: update deps and test fixes --- .github/workflows/ci.yml | 2 +- Cargo.toml | 4 +-- iced | 2 +- src/desktop.rs | 53 ++++++++++++++++++++++++---------------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50a62a50..a822642e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test --no-default-features --features "${{ matrix.features }}" + run: cargo test --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 env: RUST_BACKTRACE: full diff --git a/Cargo.toml b/Cargo.toml index 01b50733..ecb84bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ name = "cosmic" default = [ "winit", "tokio", - "a11y", + "a11y", "dbus-config", "x11", "wayland", @@ -119,7 +119,7 @@ ashpd = { version = "0.12.1", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.7" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } diff --git a/iced b/iced index b479f3e8..4516691f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b479f3e87fd54b9e80a95cf1f4d7767f9dcfbccf +Subproject commit 4516691f3582a2a8c31f886b8e6090a235f6e72c diff --git a/src/desktop.rs b/src/desktop.rs index 0d3dbb52..fe32f286 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -416,7 +416,6 @@ fn match_exec_basename( }; let basename_lower = basename.to_ascii_lowercase(); - if normalized .iter() .any(|candidate| candidate == &basename_lower) @@ -440,8 +439,7 @@ fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry { let name = context .title .as_ref() - .map(|title| title.to_string()) - .unwrap_or_else(|| context.app_id.to_string()); + .map_or_else(|| context.app_id.to_string(), |title| title.to_string()); entry.add_desktop_entry("Name".to_string(), name); entry } @@ -458,7 +456,9 @@ fn proton_or_wine_fallback( ) -> Option { let app_id = context.app_id.as_ref(); let is_proton_game = app_id == "steam_app_default"; - let is_wine_entry = app_id.ends_with(".exe"); + let is_wine_entry = std::path::Path::new(app_id) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")); if !is_proton_game && !is_wine_entry { return None; @@ -487,10 +487,6 @@ fn proton_or_wine_fallback( #[cfg(not(windows))] fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { - const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; - let mut ordered = Vec::new(); - let mut seen = HashSet::new(); - fn push_candidate(seen: &mut HashSet, ordered: &mut Vec, candidate: &str) { let trimmed = candidate.trim(); if trimmed.is_empty() { @@ -531,11 +527,11 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { } } - if trimmed.contains('.') { - if let Some(last) = trimmed.rsplit('.').next() { - if last.len() >= 2 { - push_candidate(seen, ordered, last); - } + if trimmed.contains('.') + && let Some(last) = trimmed.rsplit('.').next() + { + if last.len() >= 2 { + push_candidate(seen, ordered, last); } } @@ -546,13 +542,20 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { push_candidate(seen, ordered, &trimmed.replace('_', "-")); } - for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) { + for token in + trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace()) + { if token.len() >= 2 && token != trimmed { push_candidate(seen, ordered, token); } } } + const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; + + let mut ordered = Vec::new(); + let mut seen = HashSet::new(); + add_variants( &mut seen, &mut ordered, @@ -915,12 +918,20 @@ mod tests { let candidates = candidate_desktop_ids(&ctx); assert_eq!(candidates.first().unwrap(), "com.example.App.desktop"); - assert!(candidates.contains(&"com.example.App".to_string())); - assert!(candidates.contains(&"com-example-App".to_string())); - assert!(candidates.contains(&"com_example_App".to_string())); - assert!(candidates.contains(&"Example App".to_string())); - assert!(candidates.contains(&"Example".to_string())); - assert!(candidates.contains(&"App".to_string())); + for test in [ + "com.example.App", + "com-example-App", + "com_example_App", + "Example App", + "Example", + "App", + ] { + assert!( + candidates + .iter() + .any(|c| c.to_ascii_lowercase() == test.to_ascii_lowercase()), + ); + } } #[test] @@ -985,7 +996,7 @@ Icon=vmware-workstation\n\ let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - assert_eq!(resolved.id(), "vmware-workstation.desktop"); + assert_eq!(resolved.id(), "vmware-workstation"); } #[test] From 976e0e214f90aafae5913099475f4695e6b2841d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 3 Mar 2026 01:45:04 -0500 Subject: [PATCH 071/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 4516691f..14cefe03 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 4516691f3582a2a8c31f886b8e6090a235f6e72c +Subproject commit 14cefe034e57a189f53a10939b94d2b1d1cfdad3 From 8795c506fa9817faba87c5d0088c7a83aaab6c72 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 4 Mar 2026 10:44:42 -0500 Subject: [PATCH 072/168] chore: update iced should fix responsive widgets --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 14cefe03..40b6bfe9 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 14cefe034e57a189f53a10939b94d2b1d1cfdad3 +Subproject commit 40b6bfe9cabcaa932584f30f0710f8f69d6eb95d From ad65416551975cded91007b491b46556a45e059a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 4 Mar 2026 13:12:28 -0500 Subject: [PATCH 073/168] fix: resize border --- iced | 2 +- src/app/mod.rs | 2 +- src/applet/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iced b/iced index 40b6bfe9..fb1d5b2e 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 40b6bfe9cabcaa932584f30f0710f8f69d6eb95d +Subproject commit fb1d5b2ed88f8e56b8637b777cedb135a04098d4 diff --git a/src/app/mod.rs b/src/app/mod.rs index e11ed7ae..e137042e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -72,7 +72,7 @@ pub(crate) fn iced_settings( core.exit_on_main_window_closed = exit_on_close; if let Some(border_size) = settings.resizable { - // window_settings.resize_border = border_size as u32; + window_settings.resize_border = border_size as u32; window_settings.resizable = true; } window_settings.decorations = !settings.client_decorations; diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0cbcacab..a3f5228b 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -569,7 +569,7 @@ pub fn run(flags: App::Flags) -> iced::Result { window_settings.decorations = false; window_settings.exit_on_close_request = true; window_settings.resizable = false; - // window_settings.resize_border = 0; + window_settings.resize_border = 0; // TODO make multi-window not mandatory From 1810bedfa5db1d2d8587ec9104bb3b527d9166f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Mar=C3=ADn?= <62134857+mariinkys@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:07:26 +0100 Subject: [PATCH 074/168] fix(navbar): fill height of panel instead of shrinking --- src/app/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index e137042e..b36ec4f6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -385,9 +385,8 @@ where .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) .context_menu(self.nav_context_menu(self.core().nav_bar_context())) .into_container() - // XXX both must be shrink to avoid flex layout from ignoring it .width(iced::Length::Shrink) - .height(iced::Length::Shrink); + .height(iced::Length::Fill); if !self.core().is_condensed() { nav = nav.max_width(280); From 197049945935f0e0c2d1e42e5c0ff136dd4146ce Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 5 Mar 2026 15:38:28 -0500 Subject: [PATCH 075/168] fix: capture mouse motion and mouse interactions in overlay --- src/widget/dropdown/multi/widget.rs | 2 +- src/widget/menu/menu_inner.rs | 338 +++++++++--------- src/widget/segmented_button/widget.rs | 482 +++++++++++++------------- src/widget/toaster/widget.rs | 2 +- 4 files changed, 417 insertions(+), 407 deletions(-) diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index a46c6dcc..779c6d00 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -135,7 +135,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> self.on_selected.as_ref(), self.selections, || tree.state.downcast_mut::>(), - ) + ); } fn mouse_interaction( diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 4f97d30e..d23a1599 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -585,9 +585,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; - let menu_status = process_menu_events( + process_menu_events( self, - &event, + event, view_cursor, renderer, clipboard, @@ -629,8 +629,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if !self.is_overlay && !view_cursor.is_over(viewport) { return None; } - - let (new_root, status) = process_overlay_events( + let new_root = process_overlay_events( self, renderer, viewport_size, @@ -641,6 +640,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); + if self.is_overlay && view_cursor.is_over(viewport) { + shell.capture_event(); + } + return new_root; } @@ -680,24 +683,23 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { feature = "winit", feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(handler) = self.on_surface_action.as_ref() { - let mut root = self.window_id; - let mut depth = self.depth; - while let Some(parent) = - state.popup_id.iter().find(|(_, v)| **v == root) - { - // parent of root popup is the window, so we stop. - if depth == 0 { - break; - } - root = *parent.0; - depth = depth.saturating_sub(1); + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(handler) = self.on_surface_action.as_ref() + { + let mut root = self.window_id; + let mut depth = self.depth; + while let Some(parent) = + state.popup_id.iter().find(|(_, v)| **v == root) + { + // parent of root popup is the window, so we stop. + if depth == 0 { + break; } - shell.publish((handler)(crate::surface::Action::DestroyPopup( - root, - ))); + root = *parent.0; + depth = depth.saturating_sub(1); } + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); } state.reset(); @@ -708,7 +710,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if self.bar_bounds.contains(overlay_cursor) { state.reset(); } - }) + }); } _ => {} @@ -804,26 +806,25 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let menu_color = styling.background; r.fill_quad(menu_quad, menu_color); // draw path hightlight - if let (true, Some(active)) = (draw_path, ms.index) { - if let Some(active_layout) = children_layout + if let (true, Some(active)) = (draw_path, ms.index) + && let Some(active_layout) = children_layout .children() .nth(active.saturating_sub(start_index)) - { - let path_quad = renderer::Quad { - bounds: active_layout - .bounds() - .intersection(&viewport) - .unwrap_or_default(), - border: Border { - radius: styling.menu_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }; + { + let path_quad = renderer::Quad { + bounds: active_layout + .bounds() + .intersection(&viewport) + .unwrap_or_default(), + border: Border { + radius: styling.menu_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }; - r.fill_quad(path_quad, styling.path); - } + r.fill_quad(path_quad, styling.path); } if start_index < menu_roots.len() { // draw item @@ -894,6 +895,19 @@ impl overlay::Overlay, + cursor: mouse::Cursor, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None + } + } } impl Widget @@ -948,73 +962,74 @@ impl Widget Widget, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None } } } @@ -1331,7 +1358,7 @@ fn process_menu_events( shell, &Rectangle::default(), ); - }) + }); } #[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] @@ -1343,12 +1370,11 @@ fn process_overlay_events( view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, - _shell: &mut Shell<'_, Message>, -) -> (Option<(usize, MenuState)>, event::Status) + shell: &mut Shell<'_, Message>, +) -> Option<(usize, MenuState)> where Message: std::clone::Clone, { - use event::Status::{Captured, Ignored}; /* if no active root || pressed: return @@ -1431,8 +1457,8 @@ where state.open = false; } } - - return (new_menu_root, Captured); + shell.capture_event(); + return new_menu_root; }; let last_menu_bounds = &last_menu_state.menu_bounds; @@ -1446,7 +1472,8 @@ where { last_menu_state.index = None; - return (new_menu_root, Captured); + shell.capture_event(); + return new_menu_root; } // calc new index @@ -1461,7 +1488,7 @@ where }; if state.pressed { - return (new_menu_root, Ignored); + return new_menu_root; } let roots = active_root.iter().skip(1).fold( &menu.menu_roots[active_root[0]].children, @@ -1494,7 +1521,7 @@ where 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); - _shell.publish((menu.on_surface_action.as_ref().unwrap())({ + shell.publish((menu.on_surface_action.as_ref().unwrap())({ crate::surface::action::destroy_popup(id) })); } @@ -1555,7 +1582,8 @@ where state.menu_states.truncate(menu.depth + 1); } - (new_menu_root, Captured) + shell.capture_event(); + new_menu_root }) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index f6de999e..857d6371 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -20,7 +20,7 @@ use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, - event, keyboard, mouse, touch, window, + keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; @@ -36,7 +36,6 @@ use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; -use std::mem; use std::time::{Duration, Instant}; thread_local! { @@ -609,27 +608,26 @@ where .text .get(button) .zip(state.paragraphs.entry(button)) + && !text.is_empty() { - if !text.is_empty() { - icon_spacing = f32::from(self.button_spacing); - let paragraph = entry.or_insert_with(|| { - crate::Plain::new(Text { - content: text.to_string(), // TODO should we just use String at this point? - size: iced::Pixels(self.font_size), - bounds: Size::INFINITE, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::default(), - ellipsize: Ellipsize::default(), - line_height: self.line_height, - }) - }); + icon_spacing = f32::from(self.button_spacing); + let paragraph = entry.or_insert_with(|| { + crate::Plain::new(Text { + content: text.to_string(), // TODO should we just use String at this point? + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::default(), + ellipsize: Ellipsize::default(), + line_height: self.line_height, + }) + }); - let size = paragraph.min_bounds(); - width += size.width; - } + let size = paragraph.min_bounds(); + width += size.width; } // Add indent to measurement if found. @@ -895,10 +893,10 @@ where } // Unfocus if another segmented control was focused. - if let Some(f) = state.focused.as_ref() { - if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { - state.unfocus(); - } + if let Some(f) = state.focused.as_ref() + && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) + { + state.unfocus(); } } @@ -1162,6 +1160,9 @@ where None:: Message>, on_drop, ); + if matches!(ret, iced::event::Status::Captured) { + shell.capture_event(); + } if let Some(msg) = maybe_msg { log::trace!( target: TAB_REORDER_LOG_TARGET, @@ -1200,9 +1201,8 @@ where } Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(&id); + state.fingers_pressed.remove(id); } - _ => (), } @@ -1301,27 +1301,26 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) ) && !over_close_button + && let Some(position) = cursor_position.position() { - if let Some(position) = cursor_position.position() { - state.tab_drag_candidate = Some(TabDragCandidate { - entity: key, - bounds, - origin: position, - }); - if let Some(tab_drag) = self.tab_drag.as_ref() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - key, - position.x, - position.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - } + state.tab_drag_candidate = Some(TabDragCandidate { + entity: key, + bounds, + origin: position, + }); + if let Some(tab_drag) = self.tab_drag.as_ref() { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", + key, + position.x, + position.y, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + tab_drag.threshold + ); } } @@ -1330,40 +1329,35 @@ where } if let Some(on_activate) = self.on_activate.as_ref() { - if is_pressed(&event) { + if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); - } else if is_lifted(&event) { - if self.button_is_pressed(state, key) { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - state.pressed_item = None; - shell.capture_event(); - return; - } + } else if is_lifted(&event) && self.button_is_pressed(state, key) { + shell.publish(on_activate(key)); + state.set_focused(); + state.focused_item = Item::Tab(key); + state.pressed_item = None; + shell.capture_event(); + return; } } // Present a context menu on a right click event. - if self.context_menu.is_some() { - if let Some(on_context) = self.on_context.as_ref() { - if right_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 2) - { - state.show_context = Some(key); - state.context_cursor = - cursor_position.position().unwrap_or_default(); + if self.context_menu.is_some() + && let Some(on_context) = self.on_context.as_ref() + && (right_button_released(&event) + || (touch_lifted(&event) && fingers_pressed == 2)) + { + state.show_context = Some(key); + state.context_cursor = cursor_position.position().unwrap_or_default(); - state.menu_state.inner.with_data_mut(|data| { - data.open = true; - data.view_cursor = cursor_position; - }); + state.menu_state.inner.with_data_mut(|data| { + data.open = true; + data.view_cursor = cursor_position; + }); - shell.publish(on_context(key)); - shell.capture_event(); - return; - } - } + shell.publish(on_context(key)); + shell.capture_event(); + return; } if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = @@ -1385,57 +1379,56 @@ where } } - if self.scrollable_focus { - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { - let current = Instant::now(); + if self.scrollable_focus + && let Some(on_activate) = self.on_activate.as_ref() + && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event + { + let current = Instant::now(); - // Permit successive scroll wheel events only after a given delay. - if state.wheel_timestamp.is_none_or(|previous| { - current.duration_since(previous) > Duration::from_millis(250) - }) { - state.wheel_timestamp = Some(current); + // Permit successive scroll wheel events only after a given delay. + if state.wheel_timestamp.is_none_or(|previous| { + current.duration_since(previous) > Duration::from_millis(250) + }) { + state.wheel_timestamp = Some(current); - match delta { - ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { - let mut activate_key = None; + match delta { + ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { + let mut activate_key = None; - if *y < 0.0 { - let mut prev_key = Entity::null(); + if *y < 0.0 { + let mut prev_key = Entity::null(); - for key in self.model.order.iter().copied() { - if self.model.is_active(key) && !prev_key.is_null() { - activate_key = Some(prev_key); - } + for key in self.model.order.iter().copied() { + if self.model.is_active(key) && !prev_key.is_null() { + activate_key = Some(prev_key); + } + if self.model.is_enabled(key) { + prev_key = key; + } + } + } else if *y > 0.0 { + let mut buttons = self.model.order.iter().copied(); + while let Some(key) = buttons.next() { + if self.model.is_active(key) { + for key in buttons { if self.model.is_enabled(key) { - prev_key = key; - } - } - } else if *y > 0.0 { - let mut buttons = self.model.order.iter().copied(); - while let Some(key) = buttons.next() { - if self.model.is_active(key) { - for key in buttons { - if self.model.is_enabled(key) { - activate_key = Some(key); - break; - } - } + activate_key = Some(key); break; } } - } - - if let Some(key) = activate_key { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - shell.capture_event(); - return; + break; } } } + + if let Some(key) = activate_key { + shell.publish(on_activate(key)); + state.set_focused(); + state.focused_item = Item::Tab(key); + shell.capture_event(); + return; + } } } } @@ -1460,31 +1453,27 @@ where if let (Some(tab_drag), Some(candidate)) = (self.tab_drag.as_ref(), state.tab_drag_candidate) + && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event + && let Some(position) = cursor_position.position() + && position.distance(candidate.origin) >= tab_drag.threshold + && let Some(candidate) = state.tab_drag_candidate.take() { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { - if let Some(position) = cursor_position.position() { - if position.distance(candidate.origin) >= tab_drag.threshold { - if let Some(candidate) = state.tab_drag_candidate.take() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag threshold met entity={:?} distance={:.2} threshold={}", - candidate.entity, - position.distance(candidate.origin), - tab_drag.threshold - ); - if self.start_tab_drag( - state, - candidate.entity, - candidate.bounds, - position, - clipboard, - ) { - shell.capture_event(); - return; - } - } - } - } + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag threshold met entity={:?} distance={:.2} threshold={}", + candidate.entity, + position.distance(candidate.origin), + tab_drag.threshold + ); + if self.start_tab_drag( + state, + candidate.entity, + candidate.bounds, + position, + clipboard, + ) { + shell.capture_event(); + return; } } @@ -1504,55 +1493,53 @@ where { state.focused_visible = true; return if *modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state, shell) + self.focus_previous(state, shell); } else if modifiers.is_empty() { - self.focus_next(state, shell) + self.focus_next(state, shell); }; } - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Keyboard(keyboard::Event::KeyReleased { + if let Some(on_activate) = self.on_activate.as_ref() + && let Event::Keyboard(keyboard::Event::KeyReleased { key: keyboard::Key::Named(keyboard::key::Named::Enter), .. }) = event - { - match state.focused_item { - Item::Tab(entity) => { - shell.publish(on_activate(entity)); - } - - Item::PrevButton => { - if self.prev_tab_sensitive(state) { - state.buttons_offset -= 1; - - // If the change would cause it to be insensitive, focus the first tab. - if !self.prev_tab_sensitive(state) { - if let Some(first) = self.first_tab(state) { - state.focused_item = Item::Tab(first); - } - } - } - } - - Item::NextButton => { - if self.next_tab_sensitive(state) { - state.buttons_offset += 1; - - // If the change would cause it to be insensitive, focus the last tab. - if !self.next_tab_sensitive(state) { - if let Some(last) = self.last_tab(state) { - state.focused_item = Item::Tab(last); - } - } - } - } - - Item::None | Item::Set => (), + { + match state.focused_item { + Item::Tab(entity) => { + shell.publish(on_activate(entity)); } - shell.capture_event(); - return; + Item::PrevButton => { + if self.prev_tab_sensitive(state) { + state.buttons_offset -= 1; + + // If the change would cause it to be insensitive, focus the first tab. + if !self.prev_tab_sensitive(state) + && let Some(first) = self.first_tab(state) + { + state.focused_item = Item::Tab(first); + } + } + } + + Item::NextButton => { + if self.next_tab_sensitive(state) { + state.buttons_offset += 1; + + // If the change would cause it to be insensitive, focus the last tab. + if !self.next_tab_sensitive(state) + && let Some(last) = self.last_tab(state) + { + state.focused_item = Item::Tab(last); + } + } + } + + Item::None | Item::Set => (), } + + shell.capture_event(); } } } @@ -1794,22 +1781,22 @@ where let original_bounds = bounds; let center_y = bounds.center_y(); - if show_drop_hint_marker { - if matches!( + if show_drop_hint_marker + && matches!( drop_hint_marker, Some(DropHint { entity, side: DropSide::Before }) if entity == key - ) { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::Before, - Self::VERTICAL, - appearance.active.text_color, - ); - } + ) + { + draw_drop_indicator( + renderer, + original_bounds, + DropSide::Before, + Self::VERTICAL, + appearance.active.text_color, + ); } let menu_open = || { @@ -1882,41 +1869,41 @@ where let mut indent_padding = 0.0; // Adjust bounds by indent - if let Some(indent) = self.model.indent(key) { - if indent > 0 { - let adjustment = f32::from(indent) * f32::from(self.indent_spacing); - bounds.x += adjustment; - bounds.width -= adjustment; + if let Some(indent) = self.model.indent(key) + && indent > 0 + { + let adjustment = f32::from(indent) * f32::from(self.indent_spacing); + bounds.x += adjustment; + bounds.width -= adjustment; - // Draw indent line - if let crate::theme::SegmentedButton::FileNav = self.style { - if indent > 1 { - indent_padding = 7.0; + // Draw indent line + if let crate::theme::SegmentedButton::FileNav = self.style + && indent > 1 + { + indent_padding = 7.0; - for level in 1..indent { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: (level as f32) - .mul_add(-(self.indent_spacing as f32), bounds.x) - + indent_padding, - width: 1.0, - ..bounds - }, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - divider_background, - ); - } - - indent_padding += 4.0; - } + for level in 1..indent { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: (level as f32) + .mul_add(-(self.indent_spacing as f32), bounds.x) + + indent_padding, + width: 1.0, + ..bounds + }, + border: Border { + radius: rad_0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }, + divider_background, + ); } + + indent_padding += 4.0; } } @@ -1990,40 +1977,35 @@ where bounds.x += offset; } else { // Draw the selection indicator if widget is a segmented selection, and the item is selected. - if key_is_active { - if let crate::theme::SegmentedButton::Control = self.style { - let mut image_bounds = bounds; - image_bounds.y = center_y - 8.0; + if key_is_active && let crate::theme::SegmentedButton::Control = self.style { + let mut image_bounds = bounds; + image_bounds.y = center_y - 8.0; - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - status_appearance.text_color, - Rectangle { - width: 16.0, - height: 16.0, - ..image_bounds - }, - crate::widget::icon( - match crate::widget::common::object_select().data() { - crate::iced_core::svg::Data::Bytes(bytes) => { - crate::widget::icon::from_svg_bytes(bytes.as_ref()) - .symbolic(true) - } - crate::iced_core::svg::Data::Path(path) => { - crate::widget::icon::from_path(path.clone()) - } - }, - ), - ); + draw_icon::( + renderer, + theme, + style, + cursor, + viewport, + status_appearance.text_color, + Rectangle { + width: 16.0, + height: 16.0, + ..image_bounds + }, + crate::widget::icon(match crate::widget::common::object_select().data() { + crate::iced_core::svg::Data::Bytes(bytes) => { + crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) + } + crate::iced_core::svg::Data::Path(path) => { + crate::widget::icon::from_path(path.clone()) + } + }), + ); - let offset = 16.0 + f32::from(self.button_spacing); + let offset = 16.0 + f32::from(self.button_spacing); - bounds.x += offset; - } + bounds.x += offset; } } diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 240e4867..de47a9bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -248,7 +248,7 @@ where clipboard, shell, &layout.bounds(), - ) + ); } fn mouse_interaction( From 14a5d0c0ba60b95e5b244414da041df36148edc8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:55:53 -0500 Subject: [PATCH 076/168] fix(iced): reversed scroll direction --- examples/applet/Cargo.toml | 2 -- examples/application/Cargo.toml | 1 - iced | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 844ad8ff..f97bff44 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,8 +13,6 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] -# path = "../../" -branch = "iced-rebase" git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index b1ac1242..c842c79f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -12,7 +12,6 @@ env_logger = "0.11" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" -branch = "iced-rebase" features = [ "debug", "winit", diff --git a/iced b/iced index fb1d5b2e..b22d363f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fb1d5b2ed88f8e56b8637b777cedb135a04098d4 +Subproject commit b22d363f2c7d6485a3eddc6a54b2a652b7ded916 From 79f8337634071ac4fc32e063a5ecfc173dda4c6c Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:21:34 -0500 Subject: [PATCH 077/168] fix(iced): space key is now handled differently in iced-winit --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index b22d363f..02149769 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b22d363f2c7d6485a3eddc6a54b2a652b7ded916 +Subproject commit 02149769ec61d485ddc0bf4f07e98f0ec700420f From 3d2c018cd1df25697fa2825dbc5d040cff39389a Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:37:56 -0500 Subject: [PATCH 078/168] fix(dnd_source): rely on current cursor position for hover state --- iced | 2 +- src/widget/dnd_source.rs | 51 +++++++++++++++++----------------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/iced b/iced index 02149769..ac24bbe8 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 02149769ec61d485ddc0bf4f07e98f0ec700420f +Subproject commit ac24bbe80dd16ea586b8a0b5816066e3ba1b48fa diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 07b448a5..25900a66 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -240,7 +240,7 @@ impl match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if let Some(position) = cursor.position() { - if !state.hovered { + if !cursor.is_over(layout.bounds()) { return; } @@ -256,33 +256,27 @@ impl { if let Some(position) = cursor.position() { - if state.hovered { - // We ignore motion if we do not possess drag content by now. - if self.drag_content.is_none() { - state.left_pressed_position = None; - return; + // We ignore motion if we do not possess drag content by now. + if self.drag_content.is_none() { + state.left_pressed_position = None; + return; + } + if let Some(left_pressed_position) = state.left_pressed_position + && position.distance(left_pressed_position) > self.drag_threshold + { + if let Some(on_start) = self.on_start.as_ref() { + shell.publish(on_start.clone()); } - if let Some(left_pressed_position) = state.left_pressed_position { - if position.distance(left_pressed_position) > self.drag_threshold { - if let Some(on_start) = self.on_start.as_ref() { - shell.publish(on_start.clone()) - } - let offset = Vector::new( - left_pressed_position.x - layout.bounds().x, - left_pressed_position.y - layout.bounds().y, - ); - self.start_dnd(clipboard, state.cached_bounds, offset); - state.is_dragging = true; - state.left_pressed_position = None; - } - } - if !cursor.is_over(layout.bounds()) { - state.hovered = false; - - return; - } - } else if cursor.is_over(layout.bounds()) { - state.hovered = true; + let offset = Vector::new( + left_pressed_position.x - layout.bounds().x, + left_pressed_position.y - layout.bounds().y, + ); + self.start_dnd(clipboard, state.cached_bounds, offset); + state.is_dragging = true; + state.left_pressed_position = None; + } + if !cursor.is_over(layout.bounds()) { + return; } shell.capture_event(); } @@ -296,7 +290,6 @@ impl { @@ -306,7 +299,6 @@ impl (), @@ -422,7 +414,6 @@ impl< /// Local state of the [`MouseListener`]. #[derive(Debug, Default)] struct State { - hovered: bool, left_pressed_position: Option, is_dragging: bool, cached_bounds: Rectangle, From 03d0171bbe36664b4749c2b70297a288f35df59f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 6 Mar 2026 16:45:19 -0500 Subject: [PATCH 079/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index ac24bbe8..5f97135c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ac24bbe80dd16ea586b8a0b5816066e3ba1b48fa +Subproject commit 5f97135c3dd558cde27334a534df6f0b55ab02fa From 5eec82061592fdb5749045b71b1b2c3dbe24ee49 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 8 Mar 2026 10:09:55 +0100 Subject: [PATCH 080/168] i18n: translation updates from weblate Co-authored-by: Aman Alam Co-authored-by: Ettore Atalan Co-authored-by: Hosted Weblate Co-authored-by: Vilius Paliokas Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/lt/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pa/ Translation: Pop OS/libcosmic --- i18n/de/libcosmic.ftl | 6 ++++-- i18n/lt/libcosmic.ftl | 21 ++++++++++++++------- i18n/pa/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 1f17c924..238000f5 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -21,8 +21,8 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = Dezember { $year } -monday = Mo -tuesday = Di +monday = Montag +tuesday = Dienstag wednesday = Mittwoch thursday = Donnerstag friday = Freitag @@ -33,3 +33,5 @@ thu = Do fri = Fr sat = Sa sun = So +tue = Di +mon = Mo diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl index 6472cbd3..097b3219 100644 --- a/i18n/lt/libcosmic.ftl +++ b/i18n/lt/libcosmic.ftl @@ -2,26 +2,33 @@ february = Vasaris { $year } close = Uždaryti documenters = Dokumentuotojai november = Lapkritis { $year } -friday = Penk -tuesday = Antr +friday = Penktadienis +tuesday = Antradienis may = Gegužė { $year } -wednesday = Treč +wednesday = Trečiadienis april = Balandis { $year } -monday = Pirm +monday = Pirmadienis translators = Vertėjai artists = Menininkai license = Licencija december = Gruodis { $year } -sunday = Sekm +sunday = Sekmadienis links = Nuorodos march = Kovas { $year } june = Birželis { $year } -saturday = Šešt +saturday = Šeštadienis august = Rugpjūtis { $year } developers = Kūrėjai july = Liepa { $year } -thursday = Ketv +thursday = Ketvirtadienis september = Rugsėjis { $year } designers = Dizaineriai october = Spalis { $year } january = Sausis { $year } +mon = Pirm +tue = Antr +wed = Treč +thu = Ketv +fri = Penkt +sat = Šešt +sun = Sekm diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic.ftl index e69de29b..83d82608 100644 --- a/i18n/pa/libcosmic.ftl +++ b/i18n/pa/libcosmic.ftl @@ -0,0 +1,34 @@ +close = ਬੰਦ ਕਰੋ +license = ਲਸੰਸ +links = ਲਿੰਕ +developers = ਡਿਵੈਲਪਰ +designers = ਡਿਜ਼ਾਇਨਰ +artists = ਕਲਾਕਾਰ +translators = ਅਨੁਵਾਦਕ +documenters = ਦਸਤਾਵੇਜ਼ ਤਿਆਰ ਕਰਤਾ +january = ਜਨਵਰੀ { $year } +february = ਫਰਵਰੀ { $year } +march = ਮਾਰਚ { $year } +april = ਅਪਰੈਲ { $year } +may = ਮਈ { $year } +june = ਜੂਨ { $year } +july = ਜੁਲਾਈ { $year } +august = ਅਗਸਤ { $year } +september = ਸਤੰਬਰ { $year } +october = ਅਕਤੂਬਰ { $year } +november = ਨਵੰਬਰ { $year } +december = ਦਸੰਬਰ { $year } +monday = ਸੋਮਵਾਰ +mon = ਸੋਮ +tuesday = ਮੰਗਲਵਾਰ +tue = ਮੰਗਲ +wednesday = ਬੁੱਧਵਾਰ +wed = ਬੁੱਧ +thursday = ਵੀਰਵਾਰ +thu = ਵੀਰ +friday = ਸ਼ੁੱਕਰਵਾਰ +fri = ਸ਼ੁੱਕਰ +saturday = ਸ਼ਨਿੱਚਰਵਾਰ +sat = ਸ਼ਨਿੱਚਰ +sunday = ਐਤਵਾਰ +sun = ਐਤ From 4b92ee5f80dbc3cc6d64cf1411ade88ded8ff741 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 9 Mar 2026 16:15:03 -0400 Subject: [PATCH 081/168] chore: update iced includes fix for virtual offsets --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 5f97135c..99bc4551 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 5f97135c3dd558cde27334a534df6f0b55ab02fa +Subproject commit 99bc45511804ce94ddba880d4913e983a5f64a7f From 26f40869313f0843c436c0d8a59a05703330d571 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:33:00 -0400 Subject: [PATCH 082/168] fix(iced): fix touch event handling --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 99bc4551..1e419b2b 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 99bc45511804ce94ddba880d4913e983a5f64a7f +Subproject commit 1e419b2bc6ba5bdbb7923b1798f5292815a8c2c3 From 242fe6c4ac2229f4d1aa233e09d34234883941d0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Mar 2026 10:15:30 -0400 Subject: [PATCH 083/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 1e419b2b..d8315860 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 1e419b2bc6ba5bdbb7923b1798f5292815a8c2c3 +Subproject commit d8315860b378536dc5b2fe821b9da54934d96ff3 From b4533e3a5621edfff6cdc0c064bca61555f832b1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Mar 2026 10:38:51 -0400 Subject: [PATCH 084/168] chore: update deps --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d8315860..f0899a2a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d8315860b378536dc5b2fe821b9da54934d96ff3 +Subproject commit f0899a2a8192ed66f9d9bf00e3643194820239e6 From ce9e8b520579f8d70627093ebee74eb07c41055e Mon Sep 17 00:00:00 2001 From: Dryadxon <81884588+Dryadxon@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:15:14 +0100 Subject: [PATCH 085/168] fix(flex_row): layout::resolve swap align_items with justify_items --- src/widget/flex_row/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index b891c170..0b2e6e13 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -119,8 +119,8 @@ impl Widget for FlexR f32::from(self.column_spacing), f32::from(self.row_spacing), self.min_item_width, - self.align_items, self.justify_items, + self.align_items, self.justify_content, &mut tree.children, ) From 1dc9aa37ed8b8670217f7b9f82d40b3476204ad1 Mon Sep 17 00:00:00 2001 From: Dryadxon <81884588+Dryadxon@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:25:02 +0100 Subject: [PATCH 086/168] feat(flex_row): re-export JustifyItems --- src/widget/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index eae255bc..73004597 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -259,7 +259,7 @@ pub use id_container::{IdContainer, id_container}; #[cfg(feature = "animated-image")] pub mod frames; -pub use taffy::JustifyContent; +pub use taffy::{JustifyContent, JustifyItems}; pub mod list; #[doc(inline)] From 01e5593741c7aa7eedf0692c6087ee3fb97feeb8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Mar 2026 22:35:31 -0400 Subject: [PATCH 087/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f0899a2a..88f3b00d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f0899a2a8192ed66f9d9bf00e3643194820239e6 +Subproject commit 88f3b00d9625a3dd08ebaadb328fd119957fcd85 From c52ef976500c270b1f9b5fe488dbe5e153022ad3 Mon Sep 17 00:00:00 2001 From: Jonathan Wingrove Date: Sat, 14 Mar 2026 22:07:58 +0000 Subject: [PATCH 088/168] fix(table): Use on_item_mb_double for double-click handler instead of on_item_mb_left --- src/widget/table/widget/compact.rs | 2 +- src/widget/table/widget/standard.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 85b5cfce..db71a1af 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -145,7 +145,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { + if let Some(ref on_item_mb) = val.on_item_mb_double { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 79107074..1fa611f3 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -206,7 +206,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { + if let Some(ref on_item_mb) = val.on_item_mb_double { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area From 12cc536cd54d2b4c99f4cf8803beb260ec40dc63 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 16 Mar 2026 14:24:46 -0400 Subject: [PATCH 089/168] chore: update iced fix for tiny-skia rotation --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 88f3b00d..d79181f4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 88f3b00d9625a3dd08ebaadb328fd119957fcd85 +Subproject commit d79181f44325e63e35ef9e9653543b4bc09976bb From 9602dfd2f12b667e0afacdccd0e403d8152dde5a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 16 Mar 2026 15:59:34 -0400 Subject: [PATCH 090/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d79181f4..7491547d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d79181f44325e63e35ef9e9653543b4bc09976bb +Subproject commit 7491547d7078c8bad54cf350b1276c7f32e50df5 From adb6e304052857392b596f5b1f9732af2955a0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:23:31 +0100 Subject: [PATCH 091/168] feat(header_bar): use custom widget for layout --- src/app/mod.rs | 6 +- src/widget/header_bar.rs | 434 +++++++++++++++++++-------------------- 2 files changed, 213 insertions(+), 227 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index b36ec4f6..47900107 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -742,9 +742,6 @@ impl ApplicationExt for App { })); let content: Element<_> = if content_container { content_col - .apply(container) - .width(iced::Length::Fill) - .height(iced::Length::Fill) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { @@ -772,8 +769,7 @@ impl ApplicationExt for App { .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) - .on_double_click(crate::Action::Cosmic(Action::Maximize)) - .is_condensed(is_condensed); + .on_double_click(crate::Action::Cosmic(Action::Maximize)); if self.nav_model().is_some() { let toggle = crate::widget::nav_bar_toggle() diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1465a9d7..9ab6ff15 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,9 +5,8 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced::{Length, mouse}; -use iced_core::{Vector, Widget, widget::tree}; -use std::{borrow::Cow, cmp}; +use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; +use std::borrow::Cow; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -27,7 +26,6 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, - is_condensed: false, transparent: false, } } @@ -91,9 +89,6 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, - /// Whether the headerbar should be compact - is_condensed: bool, - /// Whether the headerbar should be transparent transparent: bool, } @@ -126,48 +121,120 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.end.push(widget.into()); self } - - /// Build the widget - #[must_use] - #[inline] - pub fn build(self) -> HeaderBarWidget<'a, Message> { - HeaderBarWidget { - header_bar_inner: self.view(), - } - } } pub struct HeaderBarWidget<'a, Message> { - header_bar_inner: Element<'a, Message>, + start: Element<'a, Message>, + center: Option>, + end: Element<'a, Message>, } -impl Widget - for HeaderBarWidget<'_, Message> +impl<'a, Message> HeaderBarWidget<'a, Message> { + pub fn new( + start: Element<'a, Message>, + center: Option>, + end: Element<'a, Message>, + ) -> Self { + Self { start, center, end } + } + + fn elems(&self) -> impl Iterator> { + std::iter::once(&self.start) + .chain(std::iter::once(&self.end)) + .chain(self.center.as_ref()) + } + + fn elems_mut(&mut self) -> impl Iterator> { + std::iter::once(&mut self.start) + .chain(std::iter::once(&mut self.end)) + .chain(self.center.as_mut()) + } +} + +impl<'a, Message: Clone + 'static> Widget + for HeaderBarWidget<'a, Message> { fn diff(&mut self, tree: &mut tree::Tree) { - tree.diff_children(&mut [&mut self.header_bar_inner]); + if let Some(center) = &mut self.center { + tree.diff_children(&mut [&mut self.start, &mut self.end, center]); + } else { + tree.diff_children(&mut [&mut self.start, &mut self.end]); + } } fn children(&self) -> Vec { - vec![tree::Tree::new(&self.header_bar_inner)] + self.elems().map(tree::Tree::new).collect() } - fn size(&self) -> iced_core::Size { - self.header_bar_inner.as_widget().size() + fn size(&self) -> Size { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let child_tree = &mut tree.children[0]; - let child = self - .header_bar_inner - .as_widget_mut() - .layout(child_tree, renderer, limits); - iced_core::layout::Node::with_children(child.size(), vec![child]) + limits: &layout::Limits, + ) -> layout::Node { + let width = limits.max().width; + let height = limits.max().height; + let gap = 8.0; + + let end_node = + self.end + .as_widget_mut() + .layout(&mut tree.children[1], renderer, &limits.loose()); + let end_width = end_node.size().width; + + let start_available = (width - end_width - gap).max(0.0); + let start_node = self.start.as_widget_mut().layout( + &mut tree.children[0], + renderer, + &layout::Limits::new(Size::ZERO, Size::new(start_available, height)), + ); + let start_width = start_node.size().width; + + let (center_node, center_x) = if let Some(center) = &mut self.center { + let slot_start = start_width + gap; + let slot_end = (width - end_width - gap).max(slot_start); + let slot_width = slot_end - slot_start; + // this instead of `node.size().width` prevents center jitter as text ellipsizes + let natural_width = center + .as_widget_mut() + .layout(&mut tree.children[2], renderer, &limits.loose()) + .size() + .width; + + let node = center.as_widget_mut().layout( + &mut tree.children[2], + renderer, + &layout::Limits::new(Size::ZERO, Size::new(slot_width, height)), + ); + + let ideal_x = (width - natural_width) / 2.0; + let max_x = (width - end_width - gap - natural_width).max(slot_start); + let center_x = ideal_x.clamp(slot_start, max_x); + (Some(node), center_x) + } else { + (None, 0.0) + }; + + let vcenter = |node: layout::Node, x: f32| -> layout::Node { + let dy = ((height - node.size().height) / 2.0).max(0.0); + node.translate(Vector::new(x, dy)) + }; + + let mut child_nodes = Vec::with_capacity(3); + child_nodes.push(vcenter(start_node, 0.0)); + child_nodes.push(vcenter(end_node, width - end_width)); + if let Some(cn) = center_node { + child_nodes.push(vcenter(cn, center_x)); + } + + layout::Node::with_children(Size::new(width, height), child_nodes) } fn draw( @@ -180,17 +247,10 @@ impl Widget cursor: iced_core::mouse::Cursor, viewport: &iced_core::Rectangle, ) { - let layout_children = layout.children().next().unwrap(); - let state_children = &tree.children[0]; - self.header_bar_inner.as_widget().draw( - state_children, - renderer, - theme, - style, - layout_children, - cursor, - viewport, - ); + for ((e, s), l) in self.elems().zip(&tree.children).zip(layout.children()) { + e.as_widget() + .draw(s, renderer, theme, style, l, cursor, viewport); + } } fn update( @@ -204,19 +264,14 @@ impl Widget shell: &mut iced_core::Shell<'_, Message>, viewport: &iced_core::Rectangle, ) { - let child_state = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - - self.header_bar_inner.as_widget_mut().update( - child_state, - event, - child_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); + for ((e, s), l) in self + .elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + { + e.as_widget_mut() + .update(s, event, l, cursor, renderer, clipboard, shell, viewport); + } } fn mouse_interaction( @@ -227,15 +282,15 @@ impl Widget viewport: &iced_core::Rectangle, renderer: &crate::Renderer, ) -> iced_core::mouse::Interaction { - let child_tree = &state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget().mouse_interaction( - child_tree, - child_layout, - cursor, - viewport, - renderer, - ) + self.elems() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, s), l)| { + e.as_widget() + .mouse_interaction(s, l, cursor, viewport, renderer) + }) + .max() + .unwrap_or(iced_core::mouse::Interaction::None) } fn operate( @@ -245,14 +300,13 @@ impl Widget renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - let child_tree = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().operate( - child_tree, - child_layout, - renderer, - operation, - ); + for ((e, s), l) in self + .elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + { + e.as_widget_mut().operate(s, l, renderer, operation); + } } fn overlay<'b>( @@ -263,15 +317,27 @@ impl Widget viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { - let child_tree = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().overlay( - child_tree, - child_layout, - renderer, - viewport, - translation, - ) + let mut layouts = layout.children(); + let mut try_overlay = |elem: &'b mut Element<'a, Message>, + state: &'b mut tree::Tree| + -> Option< + iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>, + > { + elem.as_widget_mut() + .overlay(state, layouts.next()?, renderer, viewport, translation) + }; + + if let Some(center) = &mut self.center { + let (start_slice, end_center) = state.children.split_at_mut(1); + let (end_slice, center_slice) = end_center.split_at_mut(1); + try_overlay(&mut self.start, &mut start_slice[0]) + .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) + .or_else(|| try_overlay(center, &mut center_slice[0])) + } else { + let (start_slice, end_slice) = state.children.split_at_mut(1); + try_overlay(&mut self.start, &mut start_slice[0]) + .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) + } } fn drag_destinations( @@ -281,15 +347,9 @@ impl Widget renderer: &crate::Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - if let Some((child_tree, child_layout)) = - state.children.iter().zip(layout.children()).next() - { - self.header_bar_inner.as_widget().drag_destinations( - child_tree, - child_layout, - renderer, - dnd_rectangles, - ); + for ((e, s), l) in self.elems().zip(&state.children).zip(layout.children()) { + e.as_widget() + .drag_destinations(s, l, renderer, dnd_rectangles); } } @@ -301,16 +361,22 @@ impl Widget state: &tree::Tree, p: iced::mouse::Cursor, ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - let c_state = &state.children[0]; - self.header_bar_inner - .as_widget() - .a11y_nodes(c_layout, c_state, p) + iced_accessibility::A11yTree::join( + self.elems() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, s), l)| e.as_widget().a11y_nodes(l, s, p)), + ) + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(w: HeaderBarWidget<'a, Message>) -> Self { + Element::new(w) } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { - #[allow(clippy::too_many_lines)] /// Converts the headerbar builder into an Iced element. pub fn view(mut self) -> Element<'a, Message> { let Spacing { @@ -324,154 +390,85 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - let window_control_cnt = self.on_close.is_some() as usize - + self.on_maximize.is_some() as usize - + self.on_minimize.is_some() as usize; // Also packs the window controls at the very end. - end.push(self.window_controls()); + end.push(self.window_controls(space_xxs)); - // Center content depending on window border - let padding = match self.density.unwrap_or_else(crate::config::header_size) { - Density::Compact => { - if self.maximized { - [4, 8, 4, 8] - } else { - [3, 7, 4, 7] - } - } - _ => { - if self.maximized { - [8, 8, 8, 8] - } else { - [7, 7, 8, 7] - } + let padding = if self.is_ssd { + [0, 8, 0, 8] + } else { + match ( + self.density.unwrap_or_else(crate::config::header_size), + // Center content depending on window border + self.maximized, + ) { + (Density::Compact, true) => [4, 8, 4, 8], + (Density::Compact, false) => [3, 7, 4, 7], + (_, true) => [8, 8, 8, 8], + (_, false) => [7, 7, 8, 7], } }; - let acc_count = |v: &[Element<'a, Message>]| { - v.iter().fold(0, |acc, e| { - acc + match e.as_widget().size().width { - Length::Fixed(w) if w > 30. => (w / 30.0).ceil() as usize, - _ => 1, - } - }) - }; - - let left_len = acc_count(&start); - let right_len = acc_count(&end); - - let portion = ((left_len.max(right_len + window_control_cnt) as f32 - / center.len().max(1) as f32) - .round() as u16) - .max(1); - let (left_portion, right_portion) = - if center.is_empty() && (self.title.is_empty() || self.is_condensed) { - let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32; - let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32; - if right_to_left_ratio > 2. || left_len < 1 { - (1, 2) - } else if left_to_right_ratio > 2. || right_len < 1 { - (2, 1) - } else { - (left_len as u16, (right_len + window_control_cnt) as u16) - } - } else { - (portion, portion) - }; - let title_portion = cmp::max(left_portion, right_portion) * 2; - // Creates the headerbar widget. - let mut widget = widget::row::with_capacity(3) - // If elements exist in the start region, append them here. - .push( - widget::row::with_children(start) + let start = widget::row::with_children(start) + .spacing(space_xxxs) + .align_y(iced::Alignment::Center) + .into(); + let center = if !center.is_empty() { + Some( + widget::row::with_children(center) .spacing(space_xxxs) .align_y(iced::Alignment::Center) - .apply(widget::container) - .align_x(iced::Alignment::Start) - .width(Length::FillPortion(left_portion)), + .into(), ) - // If elements exist in the center region, use them here. - // This will otherwise use the title as a widget if a title was defined. - .push_maybe(if !center.is_empty() { - Some( - widget::row::with_children(center) - .spacing(space_xxxs) - .align_y(iced::Alignment::Center) - .apply(widget::container) - .center_x(Length::Fill) - .into(), - ) - } else if !self.title.is_empty() && !self.is_condensed { - Some(self.title_widget(title_portion)) - } else { - None - }) - .push( - widget::row::with_children(end) - .spacing(space_xxs) - .align_y(iced::Alignment::Center) - .apply(widget::container) - .align_x(iced::Alignment::End) - .width(Length::FillPortion(right_portion)), + } else if !self.title.is_empty() { + Some( + widget::text::heading(self.title) + .wrapping(text::Wrapping::None) + .ellipsize(text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1))) + .into(), ) + } else { + None + }; + let end = widget::row::with_children(end) + .spacing(space_xxs) .align_y(iced::Alignment::Center) - .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) - .padding(if self.is_ssd { [0, 8, 0, 8] } else { padding }) - .spacing(8) + .into(); + + let mut widget = HeaderBarWidget::new(start, center, end) .apply(widget::container) .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, }) - .center_y(Length::Shrink) + .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) + .padding(padding) .apply(widget::mouse_area); - // Assigns a message to emit when the headerbar is dragged. - if let Some(message) = self.on_drag.clone() { + if let Some(message) = self.on_drag { widget = widget.on_drag(message); } - - // Assigns a message to emit when the headerbar is double-clicked. - if let Some(message) = self.on_maximize.clone() { + if let Some(message) = self.on_maximize { widget = widget.on_release(message); } - - if let Some(message) = self.on_double_click.clone() { + if let Some(message) = self.on_double_click { widget = widget.on_double_press(message); } - if let Some(message) = self.on_right_click.clone() { + if let Some(message) = self.on_right_click { widget = widget.on_right_press(message); } widget.into() } - fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> { - let mut title = Cow::default(); - std::mem::swap(&mut title, &mut self.title); - - widget::text::heading(title) - .wrapping(iced_core::text::Wrapping::None) - .ellipsize(iced_core::text::Ellipsize::End( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .apply(widget::container) - .center(Length::FillPortion(title_portion)) - .into() - } - /// Creates the widget for window controls. - fn window_controls(&mut self) -> Element<'a, Message> { + fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - let icon = { - widget::icon::from_name($name) - .apply(widget::button::icon) - .padding(8) - }; - - icon.class(crate::theme::Button::HeaderBar) + widget::icon::from_name($name) + .apply(widget::button::icon) + .padding(8) + .class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) @@ -482,7 +479,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .push_maybe( self.on_minimize .take() - .map(|m: Message| icon!("window-minimize-symbolic", 16, m)), + .map(|m| icon!("window-minimize-symbolic", 16, m)), ) .push_maybe(self.on_maximize.take().map(|m| { if self.maximized { @@ -496,21 +493,14 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .take() .map(|m| icon!("window-close-symbolic", 16, m)), ) - .spacing(theme::spacing().space_xxs) - .apply(widget::container) - .center_y(Length::Fill) + .spacing(spacing) + .align_y(iced::Alignment::Center) .into() } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - Element::new(headerbar.build()) - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self { - Element::new(headerbar) + headerbar.view() } } From 0bb006c5bbf7eb89491891d45bfc8f21f8eb1305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:13:46 +0100 Subject: [PATCH 092/168] fix(header_bar): add vertical SSD padding Prevents SSDs from having a gap after the rebase. --- src/widget/header_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 9ab6ff15..11b00e09 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -394,7 +394,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { end.push(self.window_controls(space_xxs)); let padding = if self.is_ssd { - [0, 8, 0, 8] + [2, 8, 2, 8] } else { match ( self.density.unwrap_or_else(crate::config::header_size), From c7ac9cfd31c8c5095d46f9322adc3e7c3208c94e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 17 Mar 2026 15:18:09 -0400 Subject: [PATCH 093/168] fix: if not in bounds, return default mouse interaction --- src/widget/segmented_button/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 857d6371..059d8387 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1596,7 +1596,7 @@ where } } - iced_core::mouse::Interaction::Idle + iced_core::mouse::Interaction::default() } #[allow(clippy::too_many_lines)] From 6c6d16d34a3572b96eabb40a86872c230d0cdd93 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:53:09 -0400 Subject: [PATCH 094/168] fix(iced): scaling issue in the cosmic-greeter lock screen --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7491547d..2d412482 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7491547d7078c8bad54cf350b1276c7f32e50df5 +Subproject commit 2d412482884ff36b30aeca656c8c43043a9f3e20 From 54bcb9ec128e86b222a3435b7c90b8c660b769bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:54:07 +0100 Subject: [PATCH 095/168] chore: update dependencies and examples --- Cargo.toml | 19 ++++----- cosmic-config/Cargo.toml | 8 ++-- cosmic-theme/Cargo.toml | 2 +- examples/cosmic/src/window/bluetooth.rs | 5 ++- examples/cosmic/src/window/demo.rs | 40 +++++++++--------- examples/cosmic/src/window/desktop.rs | 41 +++++++++---------- .../cosmic/src/window/system_and_accounts.rs | 9 ++-- src/app/cosmic.rs | 4 +- src/widget/header_bar.rs | 29 ++++++------- src/widget/settings/section.rs | 6 --- 10 files changed, 76 insertions(+), 87 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ecb84bb5..23483a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,10 +115,10 @@ x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.1", default-features = false, optional = true } +ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } -auto_enums = "0.8.7" +auto_enums = "0.8.8" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } @@ -131,17 +131,16 @@ i18n-embed = { version = "0.16.0", features = [ i18n-embed-fl = "0.10" rust-embed = "8.11.0" css-color = "0.2.8" -derive_setters = "0.1.8" +derive_setters = "0.1.9" futures = "0.3" -image = { version = "0.25.9", default-features = false, features = [ +image = { version = "0.25.10", default-features = false, features = [ "jpeg", "png", ] } -libc = { version = "0.2.180", optional = true } +libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" -raw-window-handle = "0.6" rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } @@ -151,18 +150,18 @@ slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } -tokio = { version = "1.49.0", optional = true } +tokio = { version = "1.50.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.13.2", default-features = false, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } float-cmp = "0.10.0" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus = { version = "5.13.2", default-features = false } +zbus = { version = "5.14.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -242,4 +241,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.24.0" +tempfile = "3.27.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 6103c15e..0a7653e0 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,9 +11,9 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.13.2", default-features = false, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.3", optional = true } +calloop = { version = "0.14.4", optional = true } notify = "8.2.0" ron = "0.12.0" serde = "1.0.228" @@ -22,7 +22,7 @@ iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } futures-util = { version = "0.3", optional = true } dirs.workspace = true -tokio = { version = "1.49", optional = true, features = ["time"] } +tokio = { version = "1.50", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" @@ -30,4 +30,4 @@ tracing = "0.1" xdg = "3.0" [target.'cfg(windows)'.dependencies] -known-folders = "1.4.0" +known-folders = "1.4.2" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 80f4805d..1d64912a 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -22,7 +22,7 @@ serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } ron = "0.12.0" -csscolorparser = { version = "0.8.1", features = ["serde"] } +csscolorparser = { version = "0.8.3", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 44fe7d6c..1b5892f6 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -28,13 +28,14 @@ impl State { column!( list_column().add(settings::item( "Bluetooth", - toggler(None, self.enabled, Message::Enable) + toggler(self.enabled).on_toggle(Message::Enable) )), text("Now visible as \"TODO\", just kidding") ) .spacing(8) .into(), - settings::view_section("Devices") + settings::section() + .title("Devices") .add(settings::item("No devices found", text(""))) .into(), ]) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 9ca84ef7..0d31fa93 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -258,12 +258,13 @@ impl State { match self.tab_bar.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ - settings::view_section("Debug") + settings::section() + .title("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item( "Debug layout", - toggler(None, window.debug, Message::Debug), + toggler(window.debug).on_toggle(Message::Debug), )) .add(settings::item( "Scaling Factor", @@ -276,10 +277,11 @@ impl State { .into(), ])) .into(), - settings::view_section("Controls") + settings::section() + .title("Controls") .add(settings::item( "Toggler", - toggler(None, self.toggler_value, Message::TogglerToggled), + toggler(self.toggler_value).on_toggle(Message::TogglerToggled), )) .add(settings::item( "Pick List (TODO)", @@ -299,15 +301,13 @@ impl State { .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .width(Length::Fixed(250.0)) - .height(Length::Fixed(4.0)), + .length(Length::Fixed(250.0)) + .girth(Length::Fixed(4.0)), )) - .add(settings::item_row(vec![checkbox( - "Checkbox", - self.checkbox_value, - Message::CheckboxToggled, - ) - .into()])) + .add(settings::item_row(vec![checkbox(self.checkbox_value) + .label("Checkbox") + .on_toggle(Message::CheckboxToggled) + .into()])) .add(settings::item( format!( "Spin Button (Range {}:{})", @@ -354,8 +354,7 @@ impl State { .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) - .center_x() - .width(Length::Fill) + .center_x(Length::Fill) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ @@ -424,13 +423,12 @@ impl State { ]) .padding(0) .into(), - Some(DemoView::TabC) => { - settings::view_column(vec![settings::view_section("Tab C") - .add(text("Nothing here yet").width(Length::Fill)) - .into()]) - .padding(0) - .into() - } + Some(DemoView::TabC) => settings::view_column(vec![settings::section() + .title("Tab C") + .add(text("Nothing here yet").width(Length::Fill)) + .into()]) + .padding(0) + .into(), }, container(text("Background container with some text").size(24)) .layer(cosmic_theme::Layer::Background) diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 4fa726d8..46a4e5b8 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -147,7 +147,8 @@ impl State { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::DesktopOptions), - settings::view_section("Super Key Action") + settings::section() + .title("Super Key Action") .add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item( @@ -155,38 +156,34 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::view_section("Hot Corner") + settings::section() + .title("Hot Corner") .add(settings::item( "Enable top-left hot corner for Workspaces", - toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), + toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), )) .into(), - settings::view_section("Top Panel") + settings::section() + .title("Top Panel") .add(settings::item( "Show Workspaces Button", - toggler( - None, - self.show_workspaces_button, - Message::ShowWorkspacesButton, - ), + toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), )) .add(settings::item( "Show Applications Button", - toggler( - None, - self.show_applications_button, - Message::ShowApplicationsButton, - ), + toggler(self.show_applications_button) + .on_toggle(Message::ShowApplicationsButton), )) .into(), - settings::view_section("Window Controls") + settings::section() + .title("Window Controls") .add(settings::item( "Show Minimize Button", - toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), + toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), )) .add(settings::item( "Show Maximize Button", - toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), + toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), )) .into(), ]) @@ -245,12 +242,12 @@ impl State { list_column() .add(settings::item( "Same background on all displays", - toggler(None, self.same_background, Message::SameBackground), + toggler(self.same_background).on_toggle(Message::SameBackground), )) .add(settings::item("Background fit", text("TODO"))) .add(settings::item( "Slideshow", - toggler(None, self.slideshow, Message::Slideshow), + toggler(self.slideshow).on_toggle(Message::Slideshow), )) .into(), column(image_column).spacing(16).into(), @@ -261,7 +258,8 @@ impl State { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::Wallpaper), - settings::view_section("Workspace Behavior") + settings::section() + .title("Workspace Behavior") .add(settings::item( "Dynamic workspaces", horizontal_space(Length::Fill), @@ -271,7 +269,8 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::view_section("Multi-monitor Behavior") + settings::section() + .title("Multi-monitor Behavior") .add(settings::item( "Workspaces Span Displays", horizontal_space(Length::Fill), diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index e42e643c..ed1bd004 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -69,14 +69,16 @@ impl State { list_column() .add(settings::item("Device name", text("TODO"))) .into(), - settings::view_section("Hardware") + settings::section() + .title("Hardware") .add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Memory", text("TODO"))) .add(settings::item("Processor", text("TODO"))) .add(settings::item("Graphics", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO"))) .into(), - settings::view_section("Operating System") + settings::section() + .title("Operating System") .add(settings::item("Operating system", text("TODO"))) .add(settings::item( "Operating system architecture", @@ -85,7 +87,8 @@ impl State { .add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Windowing system", text("TODO"))) .into(), - settings::view_section("Related settings") + settings::section() + .title("Related settings") .add(settings::item("Get support", text("TODO"))) .into(), ]) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index edd7b157..9566403a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -49,8 +49,8 @@ pub fn windowing_system() -> Option { WINDOWING_SYSTEM.get().copied() } -fn init_windowing_system(handle: raw_window_handle::WindowHandle) -> crate::Action { - let raw: &raw_window_handle::RawWindowHandle = handle.as_ref(); +fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { + let raw = handle.as_ref(); let system = match raw { window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 11b00e09..1c0ca2c0 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -197,7 +197,16 @@ impl<'a, Message: Clone + 'static> Widget layout::Node { + let dy = ((height - node.size().height) / 2.0).max(0.0); + node.translate(Vector::new(x, dy)) + }; + + let mut child_nodes = Vec::with_capacity(3); + child_nodes.push(vcenter(start_node, 0.0)); + child_nodes.push(vcenter(end_node, width - end_width)); + + if let Some(center) = &mut self.center { let slot_start = start_width + gap; let slot_end = (width - end_width - gap).max(slot_start); let slot_width = slot_end - slot_start; @@ -217,21 +226,8 @@ impl<'a, Message: Clone + 'static> Widget layout::Node { - let dy = ((height - node.size().height) / 2.0).max(0.0); - node.translate(Vector::new(x, dy)) - }; - - let mut child_nodes = Vec::with_capacity(3); - child_nodes.push(vcenter(start_node, 0.0)); - child_nodes.push(vcenter(end_node, width - end_width)); - if let Some(cn) = center_node { - child_nodes.push(vcenter(cn, center_x)); + child_nodes.push(vcenter(node, center_x)) } layout::Node::with_children(Size::new(width, height), child_nodes) @@ -398,8 +394,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } else { match ( self.density.unwrap_or_else(crate::config::header_size), - // Center content depending on window border - self.maximized, + self.maximized, // window border handling ) { (Density::Compact, true) => [4, 8, 4, 8], (Density::Compact, false) => [3, 7, 4, 7], diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 899826dc..ab95b5ad 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -5,12 +5,6 @@ use crate::Element; use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; -/// A section within a settings view column. -#[deprecated(note = "use `settings::section().title()` instead")] -pub fn view_section<'a, Message: 'static>(title: impl Into>) -> Section<'a, Message> { - section().title(title) -} - /// A section within a settings view column. pub fn section<'a, Message: 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) From 3da55e807440a99f6ed62edc2e7a84ca4be9b844 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 17 Mar 2026 16:45:39 -0600 Subject: [PATCH 096/168] fix(flex_row): calculate height based on nodes --- src/widget/flex_row/layout.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index ae0c28d6..166b47f4 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -162,9 +162,14 @@ pub fn resolve( }); }); + let actual_height = nodes + .iter() + .map(|node| node.bounds().y + node.bounds().height) + .fold(0.0f32, f32::max); + let size = Size { width: flex_layout.content_size.width, - height: flex_layout.content_size.height, + height: actual_height.max(flex_layout.content_size.height), }; Node::with_children(size, nodes) From 36cba695d2e4b12e4172ed8855811f6bf96223f6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Mar 2026 18:25:11 -0400 Subject: [PATCH 097/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2d412482..a3a434ac 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2d412482884ff36b30aeca656c8c43043a9f3e20 +Subproject commit a3a434ac924cb0d8f0c30ff704a01f01031c7fbb From 7a5676242259c1c743387b7a23df12bd8be1e53f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 20 Mar 2026 14:33:40 -0600 Subject: [PATCH 098/168] fix: restore width and height fill for app content --- src/app/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 47900107..5c0e95e4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -742,6 +742,8 @@ impl ApplicationExt for App { })); let content: Element<_> = if content_container { content_col + .width(iced::Length::Fill) + .height(iced::Length::Fill) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { From dc3ebaa38e6b09c5f9489d2dabc7dd31012caf40 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 19 Mar 2026 12:18:13 -0600 Subject: [PATCH 099/168] feat(segmented_button): add ellipsize support --- src/widget/segmented_button/horizontal.rs | 12 +++++ src/widget/segmented_button/vertical.rs | 9 +++- src/widget/segmented_button/widget.rs | 53 +++++++++++++++++++++-- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 3e46dd5e..5fd67649 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -213,6 +213,18 @@ where state.buttons_offset = num - state.buttons_visible; } + // Resize paragraph bounds so that text ellipsis can take effect. + if !matches!(self.width, Length::Shrink) || state.collapsed { + let num = state.buttons_visible.max(1) as f32; + let spacing = f32::from(self.spacing); + let mut width_offset = 0.0; + if state.collapsed { + width_offset = f32::from(self.button_height) * 2.0; + } + let button_width = ((num).mul_add(-spacing, size.width - width_offset) + spacing) / num; + self.resize_paragraphs(state, button_width); + } + size } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 7963e9c8..5458cd0a 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -117,10 +117,15 @@ where height += item_height; } - limits.height(Length::Fixed(height)).resolve( + let size = limits.height(Length::Fixed(height)).resolve( self.width, self.height, Size::new(width, height), - ) + ); + + // Resize paragraph bounds so that text ellipsis can take effect. + self.resize_paragraphs(state, size.width); + + size } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 059d8387..bdce1324 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -156,6 +156,8 @@ where pub(super) spacing: u16, /// LineHeight of the font. pub(super) line_height: LineHeight, + /// Ellipsize strategy for button text. + pub(super) ellipsize: Ellipsize, /// Style to draw the widget in. #[setters(into)] pub(super) style: Style, @@ -223,6 +225,7 @@ where width: Length::Fill, spacing: 0, line_height: LineHeight::default(), + ellipsize: Ellipsize::default(), style: Style::default(), context_menu: None, on_activate: None, @@ -275,7 +278,7 @@ where shaping: Shaping::Advanced, wrapping: Wrapping::None, line_height: self.line_height, - ellipsize: Ellipsize::default(), + ellipsize: self.ellipsize, }; paragraph.update(text); } else { @@ -289,7 +292,7 @@ where shaping: Shaping::Advanced, wrapping: Wrapping::None, line_height: self.line_height, - ellipsize: Ellipsize::default(), + ellipsize: self.ellipsize, }; state.paragraphs.insert(key, crate::Plain::new(text)); } @@ -621,7 +624,7 @@ where align_y: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), - ellipsize: Ellipsize::default(), + ellipsize: self.ellipsize, line_height: self.line_height, }) }); @@ -657,6 +660,50 @@ where (width, f32::from(self.button_height)) } + /// Resizes paragraph bounds based on the actual available button width so that + /// text ellipsis can take effect. Call this after `variant_layout` has populated + /// `state.internal_layout` with final button sizes. + pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) { + if matches!(self.ellipsize, Ellipsize::None) { + return; + } + + for (nth, key) in self.model.order.iter().copied().enumerate() { + if self.model.text(key).is_some_and(|text| !text.is_empty()) { + let mut non_text_width = + f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); + + if let Some(icon) = self.model.icon(key) { + non_text_width += f32::from(icon.size) + f32::from(self.button_spacing); + } else if self.model.is_active(key) { + if let crate::theme::SegmentedButton::Control = self.style { + non_text_width += 16.0 + f32::from(self.button_spacing); + } + } + + if self.model.is_closable(key) { + non_text_width += + f32::from(self.close_icon.size) + f32::from(self.button_spacing); + } + + let text_width = (available_width - non_text_width).max(0.0); + + if let Some(paragraph) = state.paragraphs.get_mut(key) { + paragraph.resize(Size::new(text_width, f32::INFINITY)); + + // Update internal_layout actual content width so that + // button_alignment centering uses the ellipsized size. + let content_width = paragraph.min_bounds().width + non_text_width + - f32::from(self.button_padding[0]) + - f32::from(self.button_padding[2]); + if let Some(entry) = state.internal_layout.get_mut(nth) { + entry.1.width = content_width; + } + } + } + } + } + pub(super) fn max_button_dimensions( &self, state: &mut LocalState, From c804d3851d28ec4ecea38a430fc66d7858af6ce1 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 20 Mar 2026 15:07:11 -0600 Subject: [PATCH 100/168] fix: don't ever draw glyphs outside of the bounds --- src/widget/segmented_button/widget.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index bdce1324..76c74f3b 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1985,7 +1985,9 @@ where // Align contents of the button to the requested `button_alignment`. { - let actual_width = state.internal_layout[nth].1.width; + // Avoid shifting content outside the left edge when the measured content is + // wider than the available button bounds (for example, non-ellipsized text). + let actual_width = state.internal_layout[nth].1.width.min(bounds.width); let offset = match self.button_alignment { Alignment::Start => None, From 141261b9bfdae30bdfd96feaf57d8ae6a48db55f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 20 Mar 2026 16:25:10 -0600 Subject: [PATCH 101/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index a3a434ac..70f54c99 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a3a434ac924cb0d8f0c30ff704a01f01031c7fbb +Subproject commit 70f54c994acb17aa247284366edc630d8514e23d From d7fd880ac6e3ea03b421541837d654bd036437ea Mon Sep 17 00:00:00 2001 From: Frederic Laing Date: Mon, 23 Mar 2026 01:11:11 +0100 Subject: [PATCH 102/168] fix(toggler): add touch input support --- src/widget/toggler.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 12bb8950..9d31ca1e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -7,7 +7,7 @@ use iced_core::{ Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, layout, mouse, renderer::{self, Renderer}, - text, + text, touch, widget::{self, Tree, tree}, window, }; @@ -239,7 +239,8 @@ impl<'a, Message> Widget for Toggler<'a, }; let state = tree.state.downcast_mut::(); match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor_position.is_over(layout.bounds()); if mouse_over { From 8e439c842ccc37a5df0821141c61766aef10c53e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 23 Mar 2026 20:17:53 -0400 Subject: [PATCH 103/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 70f54c99..f59d5354 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 70f54c994acb17aa247284366edc630d8514e23d +Subproject commit f59d5354bfc433d636c6987a60b61bc8f7a25d68 From adb3e341fc35282c0cee108b73740dcb28d4efe1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Mar 2026 12:04:00 -0400 Subject: [PATCH 104/168] fix(theme): bright colors for success, warn, destructive --- cosmic-theme/src/model/theme.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 8e1cd9f7..5db0f32c 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -986,19 +986,19 @@ impl ThemeBuilder { let success = if let Some(success) = success { success.into_color() } else { - palette.as_ref().accent_green + palette.as_ref().bright_green }; let warning = if let Some(warning) = warning { warning.into_color() } else { - palette.as_ref().accent_yellow + palette.as_ref().bright_orange }; let destructive = if let Some(destructive) = destructive { destructive.into_color() } else { - palette.as_ref().accent_red + palette.as_ref().bright_red }; let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); From 763f0da64cea86422150f522b6f0503653529a2e Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:19:39 -0400 Subject: [PATCH 105/168] fix(iced): RTL text fix --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f59d5354..a11b8282 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f59d5354bfc433d636c6987a60b61bc8f7a25d68 +Subproject commit a11b828280ccded9dd2c5d52fb4c71dc9a999e3d From a38a6f5d73294441f6ee9f141dffb541a83a8fb0 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 26 Mar 2026 18:02:10 -0600 Subject: [PATCH 106/168] fix(ci): install dependencies --- .github/workflows/pages.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 46d53ad2..4229839e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -15,6 +15,8 @@ jobs: uses: actions/checkout@v3 with: submodules: recursive + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation run: cargo doc --verbose --features tokio,winit - name: Deploy documentation From e63f3196e2ae7e9a581829675d61c3e32ce1a194 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Mar 2026 15:06:22 -0400 Subject: [PATCH 107/168] fix: MenuActive path highlight --- src/widget/menu/menu_inner.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index d23a1599..596e148e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -765,7 +765,13 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { PathHighlight::OmitActive => { !indices.is_empty() && i < indices.len() - 1 } - PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, + PathHighlight::MenuActive => { + !indices.is_empty() + && i < indices.len() + && menu_roots.len() > indices[i] + && (i < indices.len() - 1 + || !menu_roots[indices[i]].children.is_empty()) + } }); // react only to the last menu From 254c13cfc486833bf24dabf35038dd5991b1862d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Mar 2026 14:37:35 -0400 Subject: [PATCH 108/168] fix: ellipsize text in menu items --- src/widget/menu/menu_tree.rs | 46 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index bd182b9c..047df0ed 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -252,9 +252,18 @@ pub fn menu_items< let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l).into(), + widget::text(l) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), widget::space::horizontal().into(), - widget::text(key).class(key_class).into(), + widget::text(key) + .class(key_class) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), ]; if let Some(icon) = icon { @@ -275,9 +284,18 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l).into(), + widget::text(l) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), widget::space::horizontal().into(), - widget::text(key).class(key_class).into(), + widget::text(key) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .class(key_class) + .into(), ]; if let Some(icon) = icon { @@ -312,9 +330,19 @@ pub fn menu_items< .into() }, widget::space::horizontal().width(spacing.space_xxs).into(), - widget::text(label).align_x(iced::Alignment::Start).into(), + widget::text(label) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .align_x(iced::Alignment::Start) + .into(), widget::space::horizontal().into(), - widget::text(key).class(key_class).into(), + widget::text(key) + .class(key_class) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), ]; if let Some(icon) = icon { @@ -335,7 +363,11 @@ pub fn menu_items< trees.push(MenuTree::::with_children( RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ - widget::text(l.clone()).into(), + widget::text(l.clone()) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), widget::space::horizontal().into(), widget::icon::from_name("pan-end-symbolic") .size(16) From 380b341bdc57c28b8e46da13a1baf4ec996ea6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dtalo=20Dell=20Areti?= Date: Thu, 19 Feb 2026 13:18:02 -0300 Subject: [PATCH 109/168] feat(text_input): add select_range method and Task function --- src/widget/text_input/input.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 3960cee1..43db6a4d 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1125,6 +1125,14 @@ pub fn select_all(id: Id) -> Task { task::effect(Action::widget(operation::text_input::select_all(id))) } +/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given +/// [`Id`]. +pub fn select_range(id: Id, start: usize, end: usize) -> Task { + task::effect(Action::widget(operation::text_input::select_range( + id, start, end, + ))) +} + /// Computes the layout of a [`TextInput`]. #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] @@ -2782,6 +2790,12 @@ impl State { self.cursor.select_range(0, usize::MAX); } + /// Selects a range of the content of the [`TextInput`]. + #[inline] + pub fn select_range(&mut self, start: usize, end: usize) { + self.cursor.select_range(start, end); + } + pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { let position = if target > 0.0 { find_cursor_position(bounds, value, self, target) @@ -2842,8 +2856,9 @@ impl operation::TextInput for State { todo!() } + #[inline] fn select_range(&mut self, start: usize, end: usize) { - todo!() + Self::select_range(self, start, end); } } From 413e63f62a84ee9833eb13fa33ff44b27280f12a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 30 Mar 2026 18:51:33 -0400 Subject: [PATCH 110/168] chore: update features and feature gates --- Cargo.toml | 13 +++++-- iced | 2 +- src/app/action.rs | 6 +-- src/app/cosmic.rs | 63 ++++++++++++++++--------------- src/app/settings.rs | 4 +- src/core.rs | 8 ++-- src/lib.rs | 2 +- src/surface/action.rs | 18 ++++----- src/theme/style/mod.rs | 4 +- src/widget/autosize.rs | 2 +- src/widget/context_menu.rs | 36 +++++++++++++++--- src/widget/dropdown/mod.rs | 2 +- src/widget/dropdown/widget.rs | 22 +++++------ src/widget/menu/menu_bar.rs | 27 +++++++++++-- src/widget/menu/menu_inner.rs | 8 +++- src/widget/mod.rs | 2 +- src/widget/responsive_menu_bar.rs | 4 +- src/widget/text_input/input.rs | 38 +++++++++---------- 18 files changed, 159 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23483a1d..35d048ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ default = [ "a11y", "dbus-config", "x11", - "wayland", + "iced-wayland", "multi-window", ] # default = ["dbus-config", "multi-window", "a11y"] # Accessibility support @@ -80,15 +80,20 @@ tokio = [ ] # Tokio async runtime # Wayland window support -wayland = [ +iced-wayland = [ "ashpd?/wayland", "autosize", - "iced_runtime/wayland", "iced/wayland", "iced_winit/wayland", - "cctk", "surface-message", ] +wayland = [ + "iced-wayland", + "iced_runtime/cctk", + "iced_winit/cctk", + "iced/cctk", + "dep:cctk", +] surface-message = [] # multi-window support multi-window = [] diff --git a/iced b/iced index a11b8282..1fdd24ab 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a11b828280ccded9dd2c5d52fb4c71dc9a999e3d +Subproject commit 1fdd24ab995a4d65ba83cc1957e992b57cc37fcd diff --git a/src/app/action.rs b/src/app/action.rs index 05fc7cbe..fb982acb 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -5,7 +5,7 @@ use crate::surface; use crate::theme::Theme; use crate::widget::nav_bar; use crate::{config::CosmicTk, keyboard_nav}; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; @@ -69,10 +69,10 @@ pub enum Action { /// Updates the tracked window geometry. WindowResize(iced::window::Id, f32, f32), /// Tracks updates to window state. - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] WindowState(iced::window::Id, WindowState), /// Capabilities the window manager supports - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] WmCapabilities(iced::window::Id, WindowManagerCapabilities), #[cfg(feature = "xdg-portal")] DesktopSettings(crate::theme::portal::Desktop), diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9566403a..b732eee9 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -8,16 +8,16 @@ use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland")))] +#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] use iced::Application as IcedApplication; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -83,7 +83,7 @@ fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> #[derive(Default)] pub struct Cosmic { pub app: App, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub surface_views: HashMap< window::Id, ( @@ -138,7 +138,7 @@ where ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::AppSubsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -168,7 +168,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::Subsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -196,7 +196,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings()) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::AppPopup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -225,15 +225,15 @@ where iced_winit::commands::popup::get_popup(settings(&mut self.app)) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, @@ -244,7 +244,7 @@ where core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -271,7 +271,7 @@ where iced_winit::commands::popup::get_popup(settings()) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::AppWindow(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -310,7 +310,7 @@ where .discard() } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::Window(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -430,7 +430,7 @@ where } iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)), iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) @@ -443,7 +443,7 @@ where ) => { return Some(Action::SuggestedBounds(b)); } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { @@ -560,7 +560,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); } @@ -611,7 +611,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { - #[cfg(not(feature = "wayland"))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] if self .app .core() @@ -641,7 +641,7 @@ impl Cosmic { }); } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::WindowState(id, state) => { if self .app @@ -693,7 +693,7 @@ impl Cosmic { } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::WmCapabilities(id, capabilities) => { if self .app @@ -800,7 +800,7 @@ impl Cosmic { new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -946,7 +946,7 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1040,7 +1040,7 @@ impl Cosmic { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( @@ -1051,7 +1051,7 @@ impl Cosmic { ) } - #[cfg(not(feature = "wayland"))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { task = task.chain(iced_runtime::window::gain_focus(id)); } @@ -1068,7 +1068,7 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); self.tracked_windows.remove(&id); } @@ -1190,7 +1190,8 @@ impl Cosmic { #[cfg(all( feature = "wayland", feature = "multi-window", - feature = "surface-message" + feature = "surface-message", + target_os = "linux" ))] if let Some(( parent, @@ -1235,7 +1236,7 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1284,14 +1285,14 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_subsurface( &mut self, @@ -1314,7 +1315,7 @@ impl Cosmic { get_subsurface(settings) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_popup( &mut self, @@ -1336,7 +1337,7 @@ impl Cosmic { get_popup(settings) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a window surface pub fn get_window( &mut self, diff --git a/src/app/settings.rs b/src/app/settings.rs index 926181e1..5c903f09 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -16,7 +16,7 @@ pub struct Settings { pub(crate) antialiasing: bool, /// Autosize the window to fit its contents - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) autosize: bool, /// Set the application to not create a main window @@ -80,7 +80,7 @@ impl Default for Settings { fn default() -> Self { Self { antialiasing: true, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] autosize: false, no_main_window: false, client_decorations: true, diff --git a/src/core.rs b/src/core.rs index 4d50e764..970a5351 100644 --- a/src/core.rs +++ b/src/core.rs @@ -99,7 +99,7 @@ pub struct Core { pub(crate) menu_bars: HashMap, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) sync_window_border_radii_to_theme: bool, } @@ -159,7 +159,7 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, } } @@ -493,12 +493,12 @@ impl Core { } // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { self.sync_window_border_radii_to_theme = sync; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn sync_window_border_radii_to_theme(&self) -> bool { self.sync_window_border_radii_to_theme } diff --git a/src/lib.rs b/src/lib.rs index 1a579f96..aa3b7db2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] pub mod process; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub use cctk; pub mod surface; diff --git a/src/surface/action.rs b/src/surface/action.rs index 3a078ca3..50e2b4a9 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -9,25 +9,25 @@ use iced::window; use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_window( settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, @@ -60,7 +60,7 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_window( settings: impl Fn() -> window::Settings + Send + Sync + 'static, @@ -92,7 +92,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_popup( settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -126,7 +126,7 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -155,7 +155,7 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -186,7 +186,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn subsurface( settings: impl Fn( diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index a187374c..bc648a73 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -32,7 +32,7 @@ mod text_input; #[doc(inline)] pub use self::text_input::TextInput; -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub mod tooltip; -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub use tooltip::Tooltip; diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 937aabf9..69fd9c83 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -170,7 +170,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!( event, Event::PlatformSpecific(event::PlatformSpecific::Wayland( diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 200021c3..918d4da2 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,7 +3,12 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, @@ -59,7 +64,12 @@ pub struct ContextMenu<'a, Message> { } impl ContextMenu<'_, Message> { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -364,7 +374,12 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && let Some(id) = state.popup_id.remove(&self.window_id) { @@ -403,7 +418,12 @@ impl Widget state.open = true; state.view_cursor = cursor; }); - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, cursor, renderer, shell, viewport, state); } @@ -422,6 +442,7 @@ impl Widget #[cfg(all( feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -458,7 +479,12 @@ impl Widget _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.window_id != window::Id::NONE && self.on_surface_action.is_some() diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index b2d3fbed..b5fd4c06 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -50,7 +50,7 @@ pub fn popup_dropdown< let dropdown: Dropdown<'_, S, Message, AppMessage> = Dropdown::new(selections.into(), selected, on_selected); - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); dropdown diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index b6244c07..2ff9c92f 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -60,7 +60,7 @@ where action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] window_id: Option, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } @@ -96,14 +96,14 @@ where text_line_height: text::LineHeight::Relative(1.2), font: None, window_id: None, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, action_map: None, } } - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( @@ -154,7 +154,7 @@ where self } - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -268,7 +268,7 @@ where layout, cursor, shell, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] self.positioner.clone(), self.on_selected.clone(), self.selected, @@ -346,7 +346,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -545,7 +545,7 @@ pub fn update< layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, @@ -571,7 +571,7 @@ pub fn update< *hovered_guard = selected; let id = window::Id::unique(); state.popup_id = id; - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -658,7 +658,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -681,7 +681,7 @@ pub fn update< // Event wasn't processed by overlay, so cursor was clicked either outside it's // bounds or on the drop-down, either way we close the overlay. state.is_open.store(false, Ordering::Relaxed); - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -726,7 +726,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland"))] +#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 7007befb..981446e8 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -12,6 +12,7 @@ use super::{ #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -195,7 +196,12 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -230,7 +236,12 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -324,7 +335,12 @@ where self } - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -359,6 +375,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -629,6 +646,7 @@ where state.open = false; #[cfg(all( feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -652,6 +670,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -666,6 +685,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -748,6 +768,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 596e148e..74afe60f 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -7,6 +7,7 @@ use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -680,6 +681,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -966,7 +968,8 @@ impl Widget( #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -1523,7 +1527,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", 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); diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 73004597..0f607240 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -355,7 +355,7 @@ pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 5f855260..b5dd556d 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -25,7 +25,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -34,7 +34,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland")))] + #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] { ItemWidth::Static(84) } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 43db6a4d..8f6fb329 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -513,7 +513,7 @@ where } /// Sets the start dnd handler of the [`TextInput`]. - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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 @@ -1445,7 +1445,7 @@ pub fn update<'a, Message: Clone + 'static>( click.kind(), state.cursor().state(value), ) { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { let left = start.min(end); let right = end.max(start); @@ -1556,7 +1556,7 @@ pub fn update<'a, Message: Clone + 'static>( | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); let state = state(); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position update_cache(state, value); @@ -1589,7 +1589,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] 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)) @@ -1980,7 +1980,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.request_redraw(); } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { cold(); let state = state(); @@ -1991,7 +1991,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Enter { @@ -2032,7 +2032,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) if *rectangle == Some(dnd_id) => { @@ -2051,7 +2051,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); @@ -2069,9 +2069,9 @@ pub fn update<'a, Message: Clone + 'static>( return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, @@ -2089,7 +2089,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) if *rectangle == Some(dnd_id) => { @@ -2336,9 +2336,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(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(feature = "wayland"))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] let handling_dnd_offer = false; let (cursor, offset) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { @@ -2567,7 +2567,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone)] pub struct TextInputString(pub String); -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] impl AsMimeTypes for TextInputString { fn available(&self) -> Cow<'static, [String]> { Cow::Owned( @@ -2591,13 +2591,13 @@ impl AsMimeTypes for TextInputString { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] PrepareDnd(Point), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Dnd(DndAction, String), } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[derive(Debug, Default, Clone)] pub(crate) enum DndOfferState { #[default] @@ -2606,7 +2606,7 @@ pub(crate) enum DndOfferState { Dropped, } #[derive(Debug, Default, Clone)] -#[cfg(not(feature = "wayland"))] +#[cfg(not(all(feature = "wayland", target_os = "linux")))] pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. @@ -2680,7 +2680,7 @@ impl State { } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Returns the current value of the dragged text in the [`TextInput`]. #[must_use] pub fn dragged_text(&self) -> Option { From f06d15ae35204cb3bcef5a3188b5ec59a1cc9bfd Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Tue, 31 Mar 2026 16:02:52 +0100 Subject: [PATCH 111/168] feat(cosmic-theme): produce QPalette ini for more compatibility --- cosmic-theme/src/output/mod.rs | 4 + cosmic-theme/src/output/qt56ct_output.rs | 281 ++++++++++++++++++++++- cosmic-theme/src/output/qt_output.rs | 38 +-- 3 files changed, 303 insertions(+), 20 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index b2474dc1..19f7bc5b 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -46,8 +46,10 @@ impl Theme { pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); let qt_res = self.write_qt(); + let qt56ct_res = self.write_qt56ct(); gtk_res?; qt_res?; + qt56ct_res?; Ok(()) } @@ -56,8 +58,10 @@ impl Theme { pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); + let qt56ct_res = Theme::reset_qt56ct(); gtk_res?; qt_res?; + qt56ct_res?; Ok(()) } } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index 552e7fec..eccfc846 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -1,8 +1,11 @@ use crate::Theme; use configparser::ini::Ini; +use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba}; use std::{ fs::{self, File}, + io::Write, path::PathBuf, + vec, }; use super::{OutputError, qt_settings_ini_style}; @@ -15,7 +18,117 @@ impl Theme { /// Increment this value when changes to qt{5,6}ct.conf are needed. /// If the config's version is outdated, we update several sections. /// Otherwise, only the light/dark mode is updated. - const COSMIC_QT_VERSION: u64 = 1; + const COSMIC_QT_VERSION: u64 = 2; + + /// Produces a QPalette ini file for qt5ct and qt6ct. + /// + /// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf + #[must_use] + #[cold] + pub fn as_qpalette(&self) -> String { + let lightest = if self.is_dark { + self.background.on + } else { + self.background.base + }; + let darkest = if self.is_dark { + self.background.base + } else { + self.background.on + }; + let active = QPaletteGroup { + window_text: self.background.on, + button: self.button.base, + light: self.button.base.mix(lightest, 0.1), + midlight: self.button.base.mix(lightest, 0.05), + dark: self.button.base.mix(darkest, 0.1), + mid: self.button.base.mix(darkest, 0.05), + text: self.background.component.on, + bright_text: lightest, + button_text: self.button.on, + base: self.background.component.base, + window: self.background.base, + shadow: darkest, + // selection colors are swapped to fix menu bar contrast + highlight: self.background.component.selected_text, + highlighted_text: self.background.component.selected, + link: self.link_button.on, + link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2), + alternate_base: self.background.base.mix(self.accent.base, 0.05), + no_role: self.background.component.disabled, + tool_tip_base: self.background.component.base, + tool_tip_text: self.background.component.on, + placeholder_text: self.background.component.on.with_alpha(0.5), + }; + let inactive = QPaletteGroup { + window_text: active.window_text.with_alpha(0.8), + text: active.text.with_alpha(0.8), + highlighted_text: active.highlighted_text.with_alpha(0.8), + tool_tip_text: active.tool_tip_text.with_alpha(0.8), + ..active + }; + let disabled = QPaletteGroup { + button: self.button.disabled, + text: self.background.component.on_disabled, + button_text: self.button.on_disabled, + base: self.background.component.disabled, + highlighted_text: active.highlighted_text.with_alpha(0.5), + link: self.link_button.on_disabled, + link_visited: self + .link_button + .on_disabled + .mix(self.secondary.component.disabled, 0.2), + alternate_base: self.background.base.mix(self.accent.disabled, 0.05), + tool_tip_base: self.background.component.disabled, + tool_tip_text: self.background.component.on_disabled, + placeholder_text: self.background.component.on_disabled.with_alpha(0.5), + ..inactive + }; + + format!( + r#"# GENERATED BY COSMIC + +[ColorScheme] +active_colors={} +disabled_colors={} +inactive_colors={} +"#, + active.as_list(), + disabled.as_list(), + inactive.as_list(), + ) + } + + /// Writes the QPalette ini files to: + /// - `~/.config/qt6ct/colors/` + /// - `~/.config/qt5ct/colors/` + #[cold] + pub fn write_qt56ct(&self) -> Result<(), OutputError> { + let qpalette = self.as_qpalette(); + let qt5ct_res = self.write_ct("qt5ct", &qpalette); + let qt6ct_res = self.write_ct("qt6ct", &qpalette); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> { + let file_path = Self::get_qpalette_path(ct, self.is_dark)?; + let tmp_file_path = file_path.with_extension("conf.new"); + + let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; + let res = tmp_file + .write_all(qpalette.as_bytes()) + .and_then(|_| tmp_file.flush()) + .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); + if let Err(e) = res { + _ = std::fs::remove_file(&tmp_file_path); + return Err(OutputError::Io(e)); + } + + Ok(()) + } /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. #[cold] @@ -39,7 +152,7 @@ impl Theme { .map_err(OutputError::Ini)? .unwrap_or_default(); - let color_scheme_path = Self::get_qt_colors_path(is_dark)?; + let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?; let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; ini.set( @@ -91,11 +204,48 @@ impl Theme { Ok(()) } + /// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file. + #[cold] + pub fn reset_qt56ct() -> Result<(), OutputError> { + let qt5ct_res = Self::reset_ct("qt5ct"); + let qt6ct_res = Self::reset_ct("qt6ct"); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn reset_ct(ct: &str) -> Result<(), OutputError> { + let path = Self::get_conf_path(ct)?; + let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; + let mut ini = Ini::new_cs(); + ini.read(file_content).map_err(OutputError::Ini)?; + + let old_version = ini + .getuint("Appearance", "cosmic_qt_version") + .map_err(OutputError::Ini)? + .unwrap_or_default(); + if old_version == 0 { + return Ok(()); + } + + ini.remove_key("Appearance", "cosmic_qt_version"); + ini.remove_key("Appearance", "color_scheme_path"); + ini.remove_key("Appearance", "icon_theme"); + + ini.pretty_write(path, &qt_settings_ini_style()) + .map_err(OutputError::Io)?; + Ok(()) + } + /// Returns the file paths of the form `~/.config/ct/ct.conf`: /// e.g. `~/.config/qt6ct/qt6ct.conf`. /// /// The file and its parent directory are created if they don't exist. + #[cold] fn get_conf_path(ct: &str) -> Result { + assert!(ct == "qt5ct" || ct == "qt6ct"); + let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; @@ -111,4 +261,131 @@ impl Theme { Ok(file_path) } + + /// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf` + /// + /// Its parent directory is created if it doesn't exist. + #[cold] + fn get_qpalette_path(ct: &str, is_dark: bool) -> Result { + assert!(ct == "qt5ct" || ct == "qt6ct"); + + let Some(mut config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + config_dir.push(&ct); + config_dir.push("colors"); + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; + } + + let file_name = if is_dark { + "CosmicDark.conf" + } else { + "CosmicLight.conf" + }; + + Ok(config_dir.join(file_name)) + } +} + +/// Defines the different symbolic color roles used in current GUIs. +/// +/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole: +/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum +/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum +struct QPaletteGroup { + /// A general foreground color. + window_text: Srgba, + /// The general button background color. + button: Srgba, + /// Lighter than [button] color, used mostly for 3D bevel and shadow effects. + light: Srgba, + /// Between [button] and [light], used mostly for 3D bevel and shadow effects. + midlight: Srgba, + /// Darker than [button], used mostly for 3D bevel and shadow effects. + dark: Srgba, + /// Between [button] and [dark], used mostly for 3D bevel and shadow effects. + mid: Srgba, + /// The foreground color used with [base]. + text: Srgba, + /// A text color that is very different from [window_text], and contrasts well with e.g. [dark]. + /// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons. + bright_text: Srgba, + /// A foreground color used with the [button] color. + button_text: Srgba, + /// Used mostly as the background color for text entry widgets, but can also be used for other painting - + /// such as the background of combobox drop down lists and toolbar handles. + base: Srgba, + /// A general background color. + window: Srgba, + /// A very dark color, used mostly for 3D bevel and shadow effects. + /// Opaque black by default. + shadow: Srgba, + /// A color to indicate a selected item or the current item. + highlight: Srgba, + /// A text color that contrasts with [highlight]. + highlighted_text: Srgba, + /// A text color used for unvisited hyperlinks. + link: Srgba, + /// A text color used for already visited hyperlinks. + link_visited: Srgba, + /// Used as the alternate background color in views with alternating row colors. + alternate_base: Srgba, + /// No role; this special role is often used to indicate that a role has not been assigned. + no_role: Srgba, + /// Used as the background color for QToolTip and QWhatsThis. + /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. + tool_tip_base: Srgba, + /// Used as the foreground color for QToolTip and QWhatsThis. + /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. + tool_tip_text: Srgba, + /// Used as the placeholder color for various text input widgets. + placeholder_text: Srgba, + // /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct. + // /// When omitted, it defaults to [highlight]. + // accent: Srgba, +} + +impl QPaletteGroup { + /// Returns a comma-separated list of the colors as hex codes. + /// E.g. `#ff000000, #ffdcdcdc, ...` + /// + /// Any transparent colors are flattened with [base] to avoid issues with + /// the Fusion style. + fn as_list(&self) -> String { + let colors = vec![ + to_argb_hex(self.window_text.over(self.base)), + to_argb_hex(self.button.over(self.base)), + to_argb_hex(self.light.over(self.base)), + to_argb_hex(self.midlight.over(self.base)), + to_argb_hex(self.dark.over(self.base)), + to_argb_hex(self.mid.over(self.base)), + to_argb_hex(self.text.over(self.base)), + to_argb_hex(self.bright_text.over(self.base)), + to_argb_hex(self.button_text.over(self.base)), + to_argb_hex(self.base.over(self.base)), + to_argb_hex(self.window.over(self.base)), + to_argb_hex(self.shadow.over(self.base)), + to_argb_hex(self.highlight.over(self.base)), + to_argb_hex(self.highlighted_text.over(self.base)), + to_argb_hex(self.link.over(self.base)), + to_argb_hex(self.link_visited.over(self.base)), + to_argb_hex(self.alternate_base.over(self.base)), + to_argb_hex(self.no_role.over(self.base)), + to_argb_hex(self.tool_tip_base.over(self.base)), + to_argb_hex(self.tool_tip_text.over(self.base)), + to_argb_hex(self.placeholder_text.over(self.base)), + ]; + colors.join(", ") + } +} + +/// Converts a color to a hex string in the format `#AARRGGBB`. +/// Do not use [to_hex] since that uses the format `RRGGBBAA`. +fn to_argb_hex(c: Srgba) -> String { + let c_u8: Rgba = c.into_format(); + format!( + "#{:02x}{:02x}{:02x}{:02x}", + c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue + ) } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 9bca3d18..86f7ac13 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -14,10 +14,11 @@ impl Theme { /// Produces a color scheme ini file for Qt. /// /// Some high-level documentation for this file can be found at: - /// https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ + /// - https://api.kde.org/kcolorscheme.html + /// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ #[must_use] #[cold] - pub fn as_qt(&self) -> String { + pub fn as_kcolorscheme(&self) -> String { // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter let disabled_color_effects = IniColorEffects { color: self.button.disabled, @@ -41,7 +42,7 @@ impl Theme { let bg = self.background.base; // the background container - let view_colors = IniColors { + let window_colors = IniColors { background_alternate: bg.mix(self.accent.base, 0.05), background_normal: bg, decoration_focus: self.accent_text_color(), @@ -56,16 +57,17 @@ impl Theme { foreground_visited: self.accent_text_color(), }; // components inside the background container - let window_colors = IniColors { + let view_colors = IniColors { background_alternate: self.background.component.base.mix(self.accent.base, 0.05), background_normal: self.background.component.base, - ..view_colors + ..window_colors }; // selected text and items let selection_colors = { - let selected = self.background.component.selected; - let selected_text = self.background.component.selected_text; + // selection colors are swapped to fix menu bar contrast + let selected = self.background.component.selected_text; + let selected_text = self.background.component.selected; IniColors { background_alternate: selected.mix(bg, 0.5), background_normal: selected, @@ -116,10 +118,10 @@ impl Theme { }; // headers in cosmic don't have a background - let header_colors = &view_colors; - let header_colors_inactive = &view_colors; + let header_colors = &window_colors; + let header_colors_inactive = &window_colors; // tool tips, "What's This" tips, and similar elements - let tooltip_colors = &window_colors; + let tooltip_colors = &view_colors; let general_color_scheme = if self.is_dark { "CosmicDark" @@ -198,7 +200,7 @@ widgetStyle=qt6ct-style format_ini_colors(&tooltip_colors, bg), format_ini_colors(&view_colors, bg), format_ini_colors(&window_colors, bg), - format_ini_wm_colors(&view_colors, self.is_dark), + format_ini_wm_colors(&window_colors, self.is_dark), ) } @@ -212,14 +214,14 @@ widgetStyle=qt6ct-style /// Returns an `OutputError` if there is an error writing the colors file. #[cold] pub fn write_qt(&self) -> Result<(), OutputError> { - let colors = self.as_qt(); - let file_path = Self::get_qt_colors_path(self.is_dark)?; + let kcolorscheme = self.as_kcolorscheme(); + let file_path = Self::get_kcolorscheme_path(self.is_dark)?; let tmp_file_path = file_path.with_extension("colors.new"); // Write to tmp_file_path first, then move it to file_path let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; let res = tmp_file - .write_all(colors.as_bytes()) + .write_all(kcolorscheme.as_bytes()) .and_then(|_| tmp_file.flush()) .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); if let Err(e) = res { @@ -245,7 +247,7 @@ widgetStyle=qt6ct-style let kdeglobals_file = config_dir.join("kdeglobals"); let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; - let src_file = Self::get_qt_colors_path(is_dark)?; + let src_file = Self::get_kcolorscheme_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) @@ -288,7 +290,7 @@ widgetStyle=qt6ct-style } let is_dark = false; // doesn't matter since we're only reading keys - let src_file = Self::get_qt_colors_path(is_dark)?; + let src_file = Self::get_kcolorscheme_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; for (section, key_value) in src_ini.get_map_ref() { @@ -303,8 +305,8 @@ widgetStyle=qt6ct-style Ok(()) } - /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` - pub fn get_qt_colors_path(is_dark: bool) -> Result { + /// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors` + fn get_kcolorscheme_path(is_dark: bool) -> Result { let Some(mut data_dir) = dirs::data_dir() else { return Err(OutputError::MissingDataDir); }; From 1433b89e407a2f2676ceec1090224b7e27f155f7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 31 Mar 2026 14:58:46 -0400 Subject: [PATCH 112/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 1fdd24ab..be453292 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 1fdd24ab995a4d65ba83cc1957e992b57cc37fcd +Subproject commit be453292c69f3bf103b93ea27e38f57386450085 From 4541c6a275bd90f14b91bbce875212825702c9dd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 31 Mar 2026 15:09:07 -0400 Subject: [PATCH 113/168] fix: example deps --- examples/applet/Cargo.toml | 2 +- examples/application/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index f97bff44..13eff684 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,6 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" +path = "../../" default-features = false features = ["applet-token"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index c842c79f..bc037ec0 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,7 +11,7 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" +path = "../../" features = [ "debug", "winit", From d631f9d6d789304a1f01806cf2c1a0c5e93df58a Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:21:27 -0400 Subject: [PATCH 114/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index be453292..e4da5002 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit be453292c69f3bf103b93ea27e38f57386450085 +Subproject commit e4da5002ae4e9d68cc4ac777ed77b4a225659440 From 8b52592f2d6c0915d11a96af25915698e351800e Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:11:42 +0100 Subject: [PATCH 115/168] ci: test cosmic-theme --- .github/workflows/ci.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a822642e..7897eb01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,16 +33,17 @@ jobs: strategy: fail-fast: false matrix: - features: - - "" # for cosmic-comp, don't remove! - - 'winit_debug' - - 'winit_tokio' - - winit - - winit_wgpu - - wayland - - applet - - desktop,smol - - desktop,tokio + test_args: + - --no-default-features --features "" # for cosmic-comp, don't remove! + - --no-default-features --features "winit_debug" + - --no-default-features --features "winit_tokio" + - --no-default-features --features "winit" + - --no-default-features --features "winit_wgpu" + - --no-default-features --features "wayland" + - --no-default-features --features "applet" + - --no-default-features --features "desktop,smol" + - --no-default-features --features "desktop,tokio" + - -p cosmic-theme runs-on: ubuntu-22.04 steps: - name: Checkout sources @@ -66,7 +67,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 + run: cargo test ${{ matrix.test_args }} -- --test-threads=1 env: RUST_BACKTRACE: full @@ -103,7 +104,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Test example + - name: Check example run: cargo check -p "${{ matrix.examples }}" env: RUST_BACKTRACE: full From 672f9047a2666eee371ae11082800aafd1d51dd8 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:02:14 +0100 Subject: [PATCH 116/168] test: use almost::zero instead of almost::equal as per documentation "Do not use this to compare a value with a constant zero. Instead, for this you should use almost::zero." --- cosmic-theme/src/steps.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 143cf532..00a002c9 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -145,7 +145,6 @@ pub fn is_valid_srgb(c: Srgba) -> bool { #[cfg(test)] mod tests { - use almost::equal; use palette::{OklabHue, Srgba}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; @@ -173,16 +172,16 @@ mod tests { fn test_conversion_boundaries() { let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - equal(srgb.red, 0.0); - equal(srgb.blue, 0.0); - equal(srgb.green, 0.0); + almost::zero(srgb.red); + almost::zero(srgb.blue); + almost::zero(srgb.green); let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - equal(srgb.red, 1.0); - equal(srgb.blue, 1.0); - equal(srgb.green, 1.0); + almost::equal(srgb.red, 1.0); + almost::equal(srgb.blue, 1.0); + almost::equal(srgb.green, 1.0); } #[test] From e86304cf3ffaf9ea4a6f60c87898d993b4196942 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 16:59:38 +0100 Subject: [PATCH 117/168] ref: use assert_eq not assert This way, the test log can show the expected and actual result if it fails. thread 'steps::tests::test_conversion_fallback_colors' (61338) panicked at cosmic-theme/src/steps.rs:213:9: assertion `left == right` failed left: 102 right: 103 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace --- cosmic-theme/src/steps.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 00a002c9..4c3ab3d7 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -188,41 +188,41 @@ mod tests { fn test_conversion_colors() { let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 133); - assert!(srgb.green == 69); - assert!(srgb.blue == 0); + assert_eq!(srgb.red, 133); + assert_eq!(srgb.green, 69); + assert_eq!(srgb.blue, 0); let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 78); - assert!(srgb.green == 27); - assert!(srgb.blue == 15); + assert_eq!(srgb.red, 78); + assert_eq!(srgb.green, 27); + assert_eq!(srgb.blue, 15); let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 192); - assert!(srgb.green == 153); - assert!(srgb.blue == 253); + assert_eq!(srgb.red, 192); + assert_eq!(srgb.green, 153); + assert_eq!(srgb.blue, 253); } #[test] fn test_conversion_fallback_colors() { let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 255); - assert!(srgb.green == 103); - assert!(srgb.blue == 65); + assert_eq!(srgb.red, 255); + assert_eq!(srgb.green, 103); + assert_eq!(srgb.blue, 65); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 193); - assert!(srgb.green == 152); - assert!(srgb.blue == 255); + assert_eq!(srgb.red, 193); + assert_eq!(srgb.green, 152); + assert_eq!(srgb.blue, 255); let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 1); - assert!(srgb.green == 19); - assert!(srgb.blue == 0); + assert_eq!(srgb.red, 1); + assert_eq!(srgb.green, 19); + assert_eq!(srgb.blue, 0); } } From f734ccbbdeb68a5844465b2bb36a9052b54c288b Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:17:19 +0100 Subject: [PATCH 118/168] test: fix expected color value --- cosmic-theme/src/steps.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 4c3ab3d7..6ebf1015 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -210,7 +210,7 @@ mod tests { let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert_eq!(srgb.red, 255); - assert_eq!(srgb.green, 103); + assert_eq!(srgb.green, 102); assert_eq!(srgb.blue, 65); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); From 39e8300d90f7aab7a8b28e216d6631985d2de801 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 16:43:32 +0100 Subject: [PATCH 119/168] test: snapshots of kcolorscheme and qpalette AI disclosure: I asked GitHub Copilot (Claude Haiku 4.5) "What's the best way to add tests for my recently merged qt theming contributions?" It suggested the insta crate for golden testing the output strings as well as some unit tests. I implemented it myself. --- cosmic-theme/Cargo.toml | 7 + cosmic-theme/src/output/qt56ct_output.rs | 24 +++ cosmic-theme/src/output/qt_output.rs | 41 +++++ ..._output__tests__dark_default_qpalette.snap | 10 ++ ...output__tests__light_default_qpalette.snap | 10 ++ ...put__tests__dark_default_kcolorscheme.snap | 157 ++++++++++++++++++ ...ut__tests__light_default_kcolorscheme.snap | 157 ++++++++++++++++++ 7 files changed, 406 insertions(+) create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 1d64912a..7e408d8d 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -30,3 +30,10 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features configparser = "3.1.0" dirs.workspace = true thiserror = "2.0.18" + +[dev-dependencies] +insta = "1.47.2" + +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index eccfc846..43a45470 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -389,3 +389,27 @@ fn to_argb_hex(c: Srgba) -> String { c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_color_to_argb_hex() { + let color = Srgba::new(0x33, 0x55, 0x77, 0xff); + let argb = to_argb_hex(color.into()); + assert_eq!(argb, "#ff335577"); + } + + #[test] + fn test_light_default_qpalette() { + let light_default_qpalette = Theme::light_default().as_qpalette(); + insta::assert_snapshot!(light_default_qpalette); + } + + #[test] + fn test_dark_default_qpalette() { + let dark_default_qpalette = Theme::dark_default().as_qpalette(); + insta::assert_snapshot!(dark_default_qpalette); + } +} diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 86f7ac13..cd66e865 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -522,3 +522,44 @@ impl ColorEffect { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_opaque_color_to_rgb() { + let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0); + let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); + let result = to_rgb(color, bg); + assert_eq!(result, "30,50,70"); + } + + #[test] + fn test_transparent_color_to_rgb() { + let color = Srgba::new(0.0, 0.0, 0.0, 0.0); + let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); + let result = to_rgb(color, bg); + assert_eq!(result, "255,255,255"); + } + + #[test] + fn test_translucent_color_to_rgb() { + let color = Srgba::new(0.0, 0.0, 0.0, 0.9); + let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); + let result = to_rgb(color, bg); + assert_eq!(result, "26,26,26"); + } + + #[test] + fn test_light_default_kcolorscheme() { + let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme(); + insta::assert_snapshot!(light_default_kcolorscheme); + } + + #[test] + fn test_dark_default_kcolorscheme() { + let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme(); + insta::assert_snapshot!(dark_default_kcolorscheme); + } +} diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap new file mode 100644 index 00000000..15746fd0 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap @@ -0,0 +1,10 @@ +--- +source: cosmic-theme/src/output/qt56ct_output.rs +expression: dark_default_qpalette +--- +# GENERATED BY COSMIC + +[ColorScheme] +active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777 +disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090 +inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap new file mode 100644 index 00000000..c79b2c55 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap @@ -0,0 +1,10 @@ +--- +source: cosmic-theme/src/output/qt56ct_output.rs +expression: light_default_qpalette +--- +# GENERATED BY COSMIC + +[ColorScheme] +active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e +disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c +inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap new file mode 100644 index 00000000..c50f95dc --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap @@ -0,0 +1,157 @@ +--- +source: cosmic-theme/src/output/qt_output.rs +expression: dark_default_kcolorscheme +--- +# GENERATED BY COSMIC + +[ColorEffects:Disabled] +Color=43,43,43 +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +Color=27,27,27 +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate=99,208,223 +BackgroundNormal=60,60,60 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Complementary] +BackgroundAlternate=99,208,223 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Header] +BackgroundAlternate=31,36,37 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Header][Inactive] +BackgroundAlternate=31,36,37 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Selection] +BackgroundAlternate=63,118,125 +BackgroundNormal=99,208,223 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=67,67,67 +ForegroundInactive=83,138,145 +ForegroundLink=27,27,27 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=67,67,67 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Tooltip] +BackgroundAlternate=49,55,55 +BackgroundNormal=46,46,46 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:View] +BackgroundAlternate=49,55,55 +BackgroundNormal=46,46,46 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Window] +BackgroundAlternate=31,36,37 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[General] +ColorScheme=CosmicDark +Name=COSMIC Dark +shadeSortColumn=true + +[Icons] +Theme=breeze-dark + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +activeBackground=27,27,27 +activeBlend=99,208,223 +activeForeground=99,208,223 +inactiveBackground=27,27,27 +inactiveBlend=99,208,223 +inactiveForeground=99,208,223 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap new file mode 100644 index 00000000..40aacf01 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -0,0 +1,157 @@ +--- +source: cosmic-theme/src/output/qt_output.rs +expression: light_default_kcolorscheme +--- +# GENERATED BY COSMIC + +[ColorEffects:Disabled] +Color=194,194,194 +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +Color=215,215,215 +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate=0,82,90 +BackgroundNormal=173,173,173 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Complementary] +BackgroundAlternate=24,85,41 +BackgroundNormal=203,221,173 +DecorationFocus=24,85,41 +DecorationHover=24,85,41 +ForegroundActive=24,85,41 +ForegroundInactive=34,36,31 +ForegroundLink=24,85,41 +ForegroundNegative=120,41,46 +ForegroundNeutral=83,72,0 +ForegroundNormal=16,16,16 +ForegroundPositive=24,85,41 +ForegroundVisited=24,85,41 + +[Colors:Header] +BackgroundAlternate=204,208,209 +BackgroundNormal=215,215,215 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Header][Inactive] +BackgroundAlternate=204,208,209 +BackgroundNormal=215,215,215 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Selection] +BackgroundAlternate=108,149,152 +BackgroundNormal=0,82,90 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=246,246,246 +ForegroundInactive=123,164,168 +ForegroundLink=215,215,215 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=246,246,246 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Tooltip] +BackgroundAlternate=233,237,237 +BackgroundNormal=245,245,245 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:View] +BackgroundAlternate=233,237,237 +BackgroundNormal=245,245,245 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Window] +BackgroundAlternate=204,208,209 +BackgroundNormal=215,215,215 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[General] +ColorScheme=CosmicLight +Name=COSMIC Light +shadeSortColumn=true + +[Icons] +Theme=breeze + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +activeBackground=215,215,215 +activeBlend=215,215,215 +activeForeground=0,82,90 +inactiveBackground=215,215,215 +inactiveBlend=215,215,215 +inactiveForeground=0,82,90 From 9a72fe6c2da372ea940e0669ea713b56e7311133 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:49:54 +0100 Subject: [PATCH 120/168] fix: complementary should be dark not light --- cosmic-theme/src/output/qt_output.rs | 2 +- ...ut__tests__light_default_kcolorscheme.snap | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index cd66e865..5b369719 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -95,7 +95,7 @@ impl Theme { let dark = if self.is_dark { self.clone() } else { - Theme::light_config() + Theme::dark_config() .ok() .as_ref() .and_then(|conf| Theme::get_entry(conf).ok()) diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap index 40aacf01..12c511fa 100644 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -39,18 +39,18 @@ ForegroundPositive=0,87,44 ForegroundVisited=0,82,90 [Colors:Complementary] -BackgroundAlternate=24,85,41 -BackgroundNormal=203,221,173 -DecorationFocus=24,85,41 -DecorationHover=24,85,41 -ForegroundActive=24,85,41 -ForegroundInactive=34,36,31 -ForegroundLink=24,85,41 -ForegroundNegative=120,41,46 -ForegroundNeutral=83,72,0 -ForegroundNormal=16,16,16 -ForegroundPositive=24,85,41 -ForegroundVisited=24,85,41 +BackgroundAlternate=129,196,88 +BackgroundNormal=12,17,6 +DecorationFocus=129,196,88 +DecorationHover=129,196,88 +ForegroundActive=129,196,88 +ForegroundInactive=191,198,186 +ForegroundLink=129,196,88 +ForegroundNegative=253,161,160 +ForegroundNeutral=247,224,98 +ForegroundNormal=211,218,206 +ForegroundPositive=146,207,156 +ForegroundVisited=129,196,88 [Colors:Header] BackgroundAlternate=204,208,209 From c33455e9ad7e1d748f755766b6e5688c90f5f602 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 18:01:16 +0100 Subject: [PATCH 121/168] test: use default dark theme, not real system theme --- cosmic-theme/src/output/qt_output.rs | 3 +++ ...ut__tests__light_default_kcolorscheme.snap | 24 +++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 5b369719..d42d553b 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -94,6 +94,9 @@ impl Theme { let complementary_colors = { let dark = if self.is_dark { self.clone() + } else if cfg!(test) { + // For reproducible results in tests, use the default dark theme + Theme::dark_default() } else { Theme::dark_config() .ok() diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap index 12c511fa..ae2bcb66 100644 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -39,18 +39,18 @@ ForegroundPositive=0,87,44 ForegroundVisited=0,82,90 [Colors:Complementary] -BackgroundAlternate=129,196,88 -BackgroundNormal=12,17,6 -DecorationFocus=129,196,88 -DecorationHover=129,196,88 -ForegroundActive=129,196,88 -ForegroundInactive=191,198,186 -ForegroundLink=129,196,88 -ForegroundNegative=253,161,160 -ForegroundNeutral=247,224,98 -ForegroundNormal=211,218,206 -ForegroundPositive=146,207,156 -ForegroundVisited=129,196,88 +BackgroundAlternate=99,208,223 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 [Colors:Header] BackgroundAlternate=204,208,209 From 2299fba69b0116d7dc970895a78f24ebe40746a8 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 11:44:58 -0600 Subject: [PATCH 122/168] fix(text_input): RTL text cursor and highlight fixes --- src/widget/text_input/cursor.rs | 41 ++- src/widget/text_input/input.rs | 448 +++++++++++++++++++++----------- src/widget/text_input/value.rs | 33 ++- 3 files changed, 365 insertions(+), 157 deletions(-) diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index 42f52da1..3ffb535c 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -3,16 +3,19 @@ // SPDX-License-Identifier: MIT //! Track the cursor of a text input. +use iced_core::text::Affinity; + use super::value::Value; /// The cursor of a text input. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Cursor { state: State, + affinity: Affinity, } /// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum State { /// Cursor without a selection Index(usize), @@ -31,6 +34,7 @@ impl Default for Cursor { fn default() -> Self { Self { state: State::Index(0), + affinity: Affinity::Before, } } } @@ -193,4 +197,37 @@ impl Cursor { State::Selection { start, end } => start.max(end), } } + + /// Returns the current cursor [`Affinity`]. + #[must_use] + pub fn affinity(&self) -> Affinity { + self.affinity + } + + /// Sets the cursor [`Affinity`]. + pub fn set_affinity(&mut self, affinity: Affinity) { + self.affinity = affinity; + } + + /// Moves the cursor in a visual direction, accounting for RTL text. + /// + /// `forward` = `true` is visually rightward. + pub fn move_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { + match (forward ^ rtl, by_words) { + (true, false) => self.move_right(value), + (true, true) => self.move_right_by_words(value), + (false, false) => self.move_left(value), + (false, true) => self.move_left_by_words(value), + } + } + + /// Extends the selection in a visual direction, accounting for RTL text. + pub fn select_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { + match (forward ^ rtl, by_words) { + (true, false) => self.select_right(value), + (true, true) => self.select_right_by_words(value), + (false, false) => self.select_left(value), + (false, true) => self.select_left_by_words(value), + } + } } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 8f6fb329..ffb08c8b 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -937,6 +937,18 @@ where self.drag_threshold, self.always_active, ); + + let state = tree.state.downcast_mut::(); + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + state.scroll_offset = offset( + text_layout.children().next().unwrap().bounds(), + &value, + state, + ); } #[inline] @@ -1435,7 +1447,17 @@ pub fn update<'a, Message: Clone + 'static>( return; } - let target = cursor_position.x - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + cursor_position.x - text_bounds.x - alignment_offset + }; let click = mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); @@ -1454,17 +1476,30 @@ pub fn update<'a, Message: Clone + 'static>( state.value.raw(), text_layout.bounds(), left, + value, + state.cursor.affinity(), + state.scroll_offset, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( state.value.raw(), text_layout.bounds(), right, + value, + state.cursor.affinity(), + state.scroll_offset, ); - let width = right_position - left_position; + let selection_start = left_position.min(right_position); + let width = (right_position - left_position).abs(); + let alignment_offset = alignment_offset( + text_layout.bounds().width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); let selection_bounds = Rectangle { - x: text_layout.bounds().x + left_position, + x: text_layout.bounds().x + alignment_offset + selection_start + - state.scroll_offset, y: text_layout.bounds().y, width, height: text_layout.bounds().height, @@ -1492,10 +1527,11 @@ pub fn update<'a, Message: Clone + 'static>( if is_secure { state.cursor.select_all(value); } else { - let position = + let (position, affinity) = find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or(0); + .unwrap_or((0, text::Affinity::Before)); + state.cursor.set_affinity(affinity); state.cursor.select_range( value.previous_start_of_word(position), value.next_end_of_word(position), @@ -1561,7 +1597,17 @@ pub fn update<'a, Message: Clone + 'static>( // clear selection and place cursor at click position update_cache(state, value); if let Some(position) = cursor.position_over(layout.bounds()) { - let target = position.x - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + position.x - text_bounds.x - alignment_offset + }; state.setting_selection(value, text_layout.bounds(), target); } } @@ -1576,12 +1622,24 @@ pub fn update<'a, Message: Clone + 'static>( let state = state(); if matches!(state.dragging_state, Some(DraggingState::Selection)) { - let target = position.x - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + position.x - text_bounds.x - alignment_offset + }; update_cache(state, value); - let position = - find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0); + let (position, affinity) = + find_cursor_position(text_layout.bounds(), value, state, target) + .unwrap_or((0, text::Affinity::Before)); + state.cursor.set_affinity(affinity); state .cursor .select_range(state.cursor.start(value), position); @@ -1860,29 +1918,23 @@ pub fn update<'a, Message: Clone + 'static>( 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); + let rtl = state.value.raw().is_rtl(0).unwrap_or(false); + let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; + + if modifiers.shift() { + state.cursor.select_visual(false, by_words, rtl, value); } else { - state.cursor.move_left(value); + state.cursor.move_visual(false, by_words, rtl, 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); + let rtl = state.value.raw().is_rtl(0).unwrap_or(false); + let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; + + if modifiers.shift() { + state.cursor.select_visual(true, by_words, rtl, value); } else { - state.cursor.move_right(value); + state.cursor.move_visual(true, by_words, rtl, value); } } keyboard::Key::Named(keyboard::key::Named::Home) => { @@ -2016,18 +2068,27 @@ pub fn update<'a, Message: Clone + 'static>( } } if accepted { - let target = *x as f32 - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + *x as f32 - text_bounds.x - alignment_offset + }; 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); + update_cache(state, value); + let (position, affinity) = find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; + .unwrap_or((0, text::Affinity::Before)); - state.cursor.move_to(position.unwrap_or(0)); + state.cursor.set_affinity(affinity); + state.cursor.move_to(position); shell.capture_event(); return; } @@ -2038,16 +2099,25 @@ pub fn update<'a, Message: Clone + 'static>( { let state = state(); - let target = *x as f32 - text_layout.bounds().x; - // existing logic for setting the selection - let position = if target > 0.0 { - update_cache(state, value); - find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; + let target = { + let text_bounds = text_layout.bounds(); - state.cursor.move_to(position.unwrap_or(0)); + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + *x as f32 - text_bounds.x - alignment_offset + }; + // existing logic for setting the selection + update_cache(state, value); + let (position, affinity) = + find_cursor_position(text_layout.bounds(), value, state, target) + .unwrap_or((0, text::Affinity::Before)); + + state.cursor.set_affinity(affinity); + state.cursor.move_to(position); shell.capture_event(); return; } @@ -2340,7 +2410,7 @@ pub fn draw<'a, Message>( let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); #[cfg(not(all(feature = "wayland", target_os = "linux")))] let handling_dnd_offer = false; - let (cursor, offset) = if let Some(focus) = + let (cursors, offset, is_selecting) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2352,78 +2422,26 @@ pub fn draw<'a, Message>( }) { 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 (text_value_width, _) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_bounds, + position, + value, + state.cursor.affinity(), + state.scroll_offset, + ); let is_cursor_visible = handling_dnd_offer || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) .is_multiple_of(2); - if is_cursor_visible { - if dnd_icon { - (None, 0.0) - } else { - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width - offset - + if text_value_width < 0. { - actual_width - } else { - 0. - }, - 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, - }, - snap: true, - }, - 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 { + if is_cursor_visible && !dnd_icon { ( - Some(( + vec![( renderer::Quad { bounds: Rectangle { - x: text_bounds.x - + left_position - + if left_position < 0. || right_position < 0. { - actual_width - } else { - 0. - }, + x: (text_bounds.x + text_value_width).floor(), y: text_bounds.y, - width, + width: 1.0, height: text_bounds.height, }, border: Border { @@ -2438,30 +2456,101 @@ pub fn draw<'a, Message>( }, snap: true, }, - appearance.selected_fill, - )), - if end == right { - right_offset - } else { - left_offset - }, + text_color, + )], + state.scroll_offset, + false, ) + } else { + ( + Vec::<(renderer::Quad, Color)>::new(), + if dnd_icon { 0.0 } else { state.scroll_offset }, + false, + ) + } + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + if dnd_icon { + (Vec::<(renderer::Quad, Color)>::new(), 0.0, true) + } else { + let lo_byte = value.byte_index_at_grapheme(left); + let hi_byte = value.byte_index_at_grapheme(right); + + let rects = state.value.raw().highlight( + 0, + (lo_byte, text::Affinity::After), + (hi_byte, text::Affinity::Before), + ); + + let cursors: Vec<(renderer::Quad, Color)> = rects + .into_iter() + .map(|r| { + ( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + r.x, + y: text_bounds.y, + width: r.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, + }, + snap: true, + }, + appearance.selected_fill, + ) + }) + .collect(); + + (cursors, state.scroll_offset, true) } } } } else { - (None, 0.0) + let unfocused_offset = match effective_alignment(state.value.raw()) { + alignment::Horizontal::Right => { + (state.value.raw().min_width() - text_bounds.width).max(0.0) + } + _ => 0.0, + }; + + ( + Vec::<(renderer::Quad, Color)>::new(), + unfocused_offset, + false, + ) }; let render = |renderer: &mut crate::Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + if !cursors.is_empty() { + renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { + for (quad, color) in &cursors { + renderer.fill_quad(*quad, *color); + } + }); } else { renderer.with_translation(Vector::ZERO, |_| {}); } let bounds = Rectangle { - x: text_bounds.x - offset, + x: text_bounds.x + alignment_offset - offset, y: text_bounds.center_y(), width: actual_width, ..text_bounds @@ -2482,7 +2571,7 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - align_x: text::Alignment::Left, + align_x: text::Alignment::Default, align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, @@ -2495,7 +2584,11 @@ pub fn draw<'a, Message>( ); }; - renderer.with_layer(text_bounds, render); + if is_selecting { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } let trailing_icon_tree = children.get(child_index); @@ -2630,7 +2723,7 @@ pub struct State { last_click: Option, cursor: Cursor, keyboard_modifiers: keyboard::Modifiers, - // TODO: Add stateful horizontal scrolling offset + scroll_offset: f32, } #[derive(Debug, Clone, Copy)] @@ -2709,6 +2802,7 @@ impl State { last_click: None, cursor: Cursor::default(), keyboard_modifiers: keyboard::Modifiers::default(), + scroll_offset: 0.0, dirty: false, } } @@ -2797,13 +2891,11 @@ impl State { } pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { - let position = if target > 0.0 { - find_cursor_position(bounds, value, self, target) - } else { - None - }; + let (position, affinity) = find_cursor_position(bounds, value, self, target) + .unwrap_or((0, text::Affinity::Before)); - self.cursor.move_to(position.unwrap_or(0)); + self.cursor.set_affinity(affinity); + self.cursor.move_to(position); self.dragging_state = Some(DraggingState::Selection); } } @@ -2867,14 +2959,33 @@ fn measure_cursor_and_scroll_offset( paragraph: &impl text::Paragraph, text_bounds: Rectangle, cursor_index: usize, + value: &Value, + affinity: text::Affinity, + current_offset: f32, ) -> (f32, f32) { - let grapheme_position = paragraph - .grapheme_position(0, cursor_index) + let byte_index = value.byte_index_at_grapheme(cursor_index); + let position = paragraph + .cursor_position(0, byte_index, affinity) .unwrap_or(Point::ORIGIN); - let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); + // The visible window in paragraph coordinates is: + // [current_offset, current_offset + text_bounds.width] + // Keep the cursor visible with a 5px margin on each side. + let offset = if position.x > current_offset + text_bounds.width - 5.0 { + // Cursor past right edge of visible window → scroll left + (position.x + 5.0) - text_bounds.width + } else if position.x < current_offset + 5.0 { + // Cursor past left edge of visible window → scroll right + position.x - 5.0 + } else { + // Cursor is within visible window → keep current scroll + current_offset + }; - (grapheme_position.x, offset) + let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0); + let offset = offset.clamp(0.0, max_offset); + + (position.x, offset) } /// Computes the position of the text cursor at the given X coordinate of @@ -2885,23 +2996,23 @@ fn find_cursor_position( value: &Value, state: &State, x: f32, -) -> Option { - let offset = offset(text_bounds, value, state); - let value = value.to_string(); +) -> Option<(usize, text::Affinity)> { + let value_str = value.to_string(); - let char_offset = state - .value - .raw() - .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) - .map(text::Hit::cursor)?; + let hit = state.value.raw().hit_test(Point::new( + x + state.scroll_offset, + text_bounds.height / 2.0, + ))?; + let char_offset = hit.cursor(); + let affinity = hit.affinity(); - Some( - unicode_segmentation::UnicodeSegmentation::graphemes( - &value[..char_offset.min(value.len())], - true, - ) - .count(), + let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes( + &value_str[..char_offset.min(value_str.len())], + true, ) + .count(); + + Some((grapheme_count, affinity)) } #[inline(never)] @@ -2928,7 +3039,7 @@ fn replace_paragraph( content: value.to_string(), bounds, size: text_size, - align_x: text::Alignment::Left, + align_x: text::Alignment::Default, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2961,11 +3072,48 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { cursor::State::Selection { end, .. } => end, }; - let (_, offset) = - measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position); + let (_, offset) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_bounds, + focus_position, + value, + state.cursor().affinity(), + state.scroll_offset, + ); offset } else { - 0.0 + match effective_alignment(state.value.raw()) { + alignment::Horizontal::Right => { + (state.value.raw().min_width() - text_bounds.width).max(0.0) + } + _ => 0.0, + } + } +} + +#[inline(never)] +fn alignment_offset( + text_bounds_width: f32, + text_min_width: f32, + alignment: alignment::Horizontal, +) -> f32 { + if text_min_width > text_bounds_width { + 0.0 + } else { + match alignment { + alignment::Horizontal::Left => 0.0, + alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0, + alignment::Horizontal::Right => text_bounds_width - text_min_width, + } + } +} + +#[inline(never)] +fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal { + if paragraph.is_rtl(0).unwrap_or(false) { + alignment::Horizontal::Right + } else { + alignment::Horizontal::Left } } diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 900aac0f..9faff4ac 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -132,11 +132,34 @@ impl Value { graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(), } } -} -impl ToString for Value { - #[inline] - fn to_string(&self) -> String { - self.graphemes.concat() + /// Converts a grapheme index to a byte index in the underlying string. + #[must_use] + pub fn byte_index_at_grapheme(&self, grapheme_index: usize) -> usize { + self.graphemes[..grapheme_index.min(self.graphemes.len())] + .iter() + .map(|g| g.len()) + .sum() + } + + /// Converts a byte index to a grapheme index. + #[must_use] + pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { + let mut bytes = 0; + for (i, g) in self.graphemes.iter().enumerate() { + if bytes >= byte_index { + return i; + } + bytes += g.len(); + } + + self.graphemes.len() + } +} + +impl std::fmt::Display for Value { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.graphemes.concat()) } } From e1738d2ea7c3a2df2584a7cf7f098681c2b34c86 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 11:49:12 -0600 Subject: [PATCH 123/168] fix(text_input): keyboard shortcuts when keyboard is a different language Matches what Iced does --- src/widget/text_input/input.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index ffb08c8b..a86e4b6e 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1719,13 +1719,10 @@ pub fn update<'a, Message: Clone + 'static>( focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - // Check if Ctrl+A/C/V/X was pressed. - if state.keyboard_modifiers == keyboard::Modifiers::COMMAND - || state.keyboard_modifiers - == keyboard::Modifiers::COMMAND | keyboard::Modifiers::CAPS_LOCK - { - match key.as_ref() { - keyboard::Key::Character("c") | keyboard::Key::Character("C") => { + // Check if Ctrl/Command+A/C/V/X was pressed. + if state.keyboard_modifiers.command() { + match key.to_latin(*physical_key) { + Some('c') => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1737,7 +1734,7 @@ pub fn update<'a, Message: Clone + 'static>( } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut - keyboard::Key::Character("x") | keyboard::Key::Character("X") => { + Some('x') => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1756,7 +1753,7 @@ pub fn update<'a, Message: Clone + 'static>( } } } - keyboard::Key::Character("v") | keyboard::Key::Character("V") => { + Some('v') => { let content = if let Some(content) = state.is_pasting.take() { content } else { @@ -1801,7 +1798,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } - keyboard::Key::Character("a") | keyboard::Key::Character("A") => { + Some('a') => { state.cursor.select_all(value); shell.capture_event(); return; From 22661fd76459a279fc5837ed61abb56866e2f988 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 15:25:10 -0600 Subject: [PATCH 124/168] chore: udpate iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index e4da5002..84f32108 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit e4da5002ae4e9d68cc4ac777ed77b4a225659440 +Subproject commit 84f3210819c03f5393fe4dcc404ab9532b941c70 From aef328238fcdaaa17e05f67fc53615ce64a547e0 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 13:30:36 -0600 Subject: [PATCH 125/168] fix(editable): the UX is closer to design now This fixes the unresponsive trailing icon and changes the behavior to be closer to the UI/UX design. --- src/widget/text_input/input.rs | 146 ++++++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 38 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index a86e4b6e..2c788235 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -66,18 +66,20 @@ pub fn editable_input<'a, Message: Clone + 'static>( 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" - }); - + // The trailing icon is a placeholder; diff() rebuilds it reactively + // based on the current is_read_only state and value content. TextInput::new(placeholder, text) .style(crate::theme::TextInput::EditableText) .editable() .editing(editing) .on_toggle_edit(on_toggle_edit) - .trailing_icon(icon.size(16).into()) + .trailing_icon( + crate::widget::icon::from_name("edit-symbolic") + .size(16) + .apply(crate::widget::container) + .padding(8) + .into(), + ) } /// Creates a new search [`TextInput`]. @@ -666,7 +668,36 @@ where } } - self.is_read_only = state.is_read_only; + if self.is_editable_variant { + if !state.is_focused() { + // Not yet interacted, use the widget's value + state.is_read_only = self.is_read_only; + } else { + // Already interacted, use the state + self.is_read_only = state.is_read_only; + } + + let editing = !self.is_read_only; + let icon_name = if editing { + if self.value.is_empty() { + "window-close-symbolic" + } else { + "edit-clear-symbolic" + } + } else { + "edit-symbolic" + }; + + self.trailing_icon = Some( + crate::widget::icon::from_name(icon_name) + .size(16) + .apply(crate::widget::container) + .padding(8) + .into(), + ); + } else { + self.is_read_only = state.is_read_only; + } // Stop pasting if input becomes disabled if !self.manage_value && self.on_input.is_none() { @@ -855,9 +886,6 @@ where 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)); } 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; @@ -1018,9 +1046,7 @@ where index += 1; } - if let (Some(trailing_icon), Some(tree)) = - (self.trailing_icon.as_ref(), state.children.get(index)) - { + if self.trailing_icon.is_some() { let mut children = layout.children(); children.next(); // skip if there is no leading icon @@ -1030,13 +1056,21 @@ where 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, - ); + if self.is_editable_variant { + return mouse::Interaction::Pointer; + } + + if let Some((trailing_icon, tree)) = + self.trailing_icon.as_ref().zip(state.children.get(index)) + { + return trailing_icon.as_widget().mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ); + } } } let mut children = layout.children(); @@ -1426,21 +1460,54 @@ pub fn update<'a, Message: Clone + 'static>( && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds())) { if is_editable_variant { - state.is_read_only = !state.is_read_only; - state.move_cursor_to_end(); + let has_content = !unsecured_value.is_empty(); + let is_editing = !state.is_read_only; - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(!state.is_read_only)); + if is_editing && has_content { + if let Some(on_input) = on_input { + shell.publish((on_input)(String::new())); + } + + if manage_value { + *unsecured_value = Value::new(""); + state.tracked_value = unsecured_value.clone(); + + let cleared_value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value.clone() + }; + + update_cache(state, &cleared_value); + } + + state.move_cursor_to_end(); + } else if is_editing { + // Close: toggle back to read-only and unfocus. + state.is_read_only = true; + state.unfocus(); + + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(false)); + } + } else { + // Edit: toggle to editing, select all, and focus. + state.is_read_only = false; + state.cursor.select_range(0, value.len()); + + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(true)); + } + + let now = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(now)); + state.is_focused = Some(Focus { + updated_at: now, + now, + focused: true, + needs_update: false, + }); } - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); } shell.capture_event(); @@ -1550,15 +1617,18 @@ 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() - && matches!(state.dragging_state, None | Some(DraggingState::Selection)) + if matches!(state.dragging_state, None | Some(DraggingState::Selection)) + && (!state.is_focused() || (is_editable_variant && state.is_read_only)) { - if let Some(on_focus) = on_focus { - shell.publish(on_focus.clone()); + if !state.is_focused() { + if let Some(on_focus) = on_focus { + shell.publish(on_focus.clone()); + } } if state.is_read_only { state.is_read_only = false; + state.cursor.select_range(0, value.len()); if let Some(on_toggle_edit) = on_toggle_edit { let message = (on_toggle_edit)(true); shell.publish(message); From 0ba668eb52908e5b10f65971d7a4b6395dc194ae Mon Sep 17 00:00:00 2001 From: TobyDig <53296459+TobyDig@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:32:36 +1100 Subject: [PATCH 126/168] fix(desktop): use `-e` argument for spawning desktop entries with a terminal --- src/desktop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop.rs b/src/desktop.rs index fe32f286..98ce7d4b 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -789,7 +789,7 @@ pub async fn spawn_desktop_exec( }) .unwrap_or_else(|| String::from("cosmic-term")); - term_exec = format!("{term} -- {}", exec.as_ref()); + term_exec = format!("{term} -e {}", exec.as_ref()); &term_exec } else { exec.as_ref() From f6eb314606f77adfa7199338a89b17f3d17d136c Mon Sep 17 00:00:00 2001 From: KENZ Date: Thu, 2 Apr 2026 07:35:57 +0900 Subject: [PATCH 127/168] feat(text_input): minimal IME support for COSMIC specific text widgets --- src/widget/text_input/input.rs | 104 ++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 2c788235..cd93a7d7 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -22,10 +22,11 @@ use iced::Limits; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::mime::AsMimeTypes; use iced_core::event::{self, Event}; +use iced_core::input_method::{self, InputMethod, Preedit}; 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::text::{self, Affinity, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::touch; use iced_core::widget::Id; @@ -2083,6 +2084,66 @@ pub fn update<'a, Message: Clone + 'static>( 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(); + return; + } + input_method::Event::Preedit(content, selection) => { + if state.is_focused.is_some() { + state.preedit = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + text_size: Some(size.into()), + }); + shell.capture_event(); + return; + } + } + input_method::Event::Commit(text) => { + let Some(focus) = &mut state.is_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)) => { let state = state(); @@ -2095,6 +2156,8 @@ pub fn update<'a, Message: Clone + 'static>( now.checked_add(Duration::from_millis(millis_until_redraw as u64)) .unwrap_or(*now), )); + + shell.request_input_method(&input_method(state, text_layout, unsecured_value)); } else if always_active { shell.request_redraw(); } @@ -2269,6 +2332,43 @@ pub fn update<'a, Message: Clone + 'static>( } } +fn input_method<'b>( + state: &'b State, + text_layout: Layout<'_>, + value: &Value, +) -> InputMethod<&'b str> { + if state.is_focused() { + } else { + return InputMethod::Disabled; + }; + + let text_bounds = text_layout.bounds(); + let cursor_index = match state.cursor.state(value) { + cursor::State::Index(position) => position, + cursor::State::Selection { start, end } => start.min(end), + }; + let (cursor, offset) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_bounds, + cursor_index, + value, + state.cursor.affinity(), + state.scroll_offset, + ); + InputMethod::Enabled { + cursor: Rectangle::new( + Point::new(text_bounds.x + cursor - offset, text_bounds.y), + Size::new(1.0, text_bounds.height), + ), + purpose: if state.is_secure { + input_method::Purpose::Secure + } else { + input_method::Purpose::Normal + }, + preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), + } +} + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -2789,6 +2889,7 @@ pub struct State { is_pasting: Option, last_click: Option, cursor: Cursor, + preedit: Option, keyboard_modifiers: keyboard::Modifiers, scroll_offset: f32, } @@ -2868,6 +2969,7 @@ impl State { is_pasting: None, last_click: None, cursor: Cursor::default(), + preedit: None, keyboard_modifiers: keyboard::Modifiers::default(), scroll_offset: 0.0, dirty: false, From 12be83a8ef58019a1bd31ead100040244bd05f16 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 20:12:12 -0600 Subject: [PATCH 128/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 84f32108..42e3afb5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 84f3210819c03f5393fe4dcc404ab9532b941c70 +Subproject commit 42e3afb5686eff08c78c9292bb83c36d5c8f5146 From 61e5d882ae877f39b3389e17da48f516fdcc4582 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 26 Mar 2026 19:27:50 -0600 Subject: [PATCH 129/168] fix(ci): only document libcosmic, no dependency --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4229839e..e48570ba 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -18,7 +18,7 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation - run: cargo doc --verbose --features tokio,winit + run: cargo doc --no-deps --verbose --features tokio,winit - name: Deploy documentation uses: peaceiris/actions-gh-pages@v3 with: From 7a02c9a296c10469d9061391657f71aa33b3936b Mon Sep 17 00:00:00 2001 From: GroobleDierne Date: Fri, 30 Jan 2026 23:33:52 +0100 Subject: [PATCH 130/168] fix(color palette): avoid duplicates --- src/widget/color_picker/mod.rs | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index d484bb62..318e943b 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -4,7 +4,6 @@ //! Widgets for selecting colors with a color picker. use std::borrow::Cow; -use std::iter; use std::rc::Rc; use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -93,8 +92,6 @@ pub struct ColorPickerModel { #[setters(skip)] active_color: palette::Hsv, #[setters(skip)] - save_next: Option, - #[setters(skip)] input_color: String, #[setters(skip)] applied_color: Option, @@ -128,7 +125,6 @@ impl ColorPickerModel { .insert(move |b| b.text(rgb.clone())) .build(), active_color: hsv, - save_next: None, input_color: color_to_string(hsv, true), applied_color: initial, fallback_color, @@ -159,22 +155,26 @@ impl ColorPickerModel { ) } + fn update_recent_colors(&mut self, new_color: Color) { + if let Some(pos) = self.recent_colors.iter().position(|c| *c == new_color) { + self.recent_colors.remove(pos); + } + self.recent_colors.insert(0, new_color); + self.recent_colors.truncate(MAX_RECENT); + } + pub fn update(&mut self, update: ColorPickerUpdate) -> Task { match update { ColorPickerUpdate::ActiveColor(c) => { self.must_clear_cache.store(true, Ordering::SeqCst); self.input_color = color_to_string(c, self.is_hex()); - if let Some(to_save) = self.save_next.take() { - self.recent_colors.insert(0, to_save); - self.recent_colors.truncate(MAX_RECENT); - } self.active_color = c; self.copied_at = None; } - ColorPickerUpdate::AppliedColor => { + ColorPickerUpdate::AppliedColor | ColorPickerUpdate::ActionFinished => { let srgb = palette::Srgb::from_color(self.active_color); if let Some(applied_color) = self.applied_color.take() { - self.recent_colors.push(applied_color); + self.update_recent_colors(applied_color); } self.applied_color = Some(Color::from(srgb)); self.active = false; @@ -215,21 +215,12 @@ impl ColorPickerModel { palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue)); } } - ColorPickerUpdate::ActionFinished => { - let srgb = palette::Srgb::from_color(self.active_color); - if let Some(applied_color) = self.applied_color.take() { - self.recent_colors.push(applied_color); - } - self.applied_color = Some(Color::from(srgb)); - self.active = false; - self.save_next = Some(Color::from(srgb)); - } ColorPickerUpdate::ToggleColorPicker => { self.must_clear_cache.store(true, Ordering::SeqCst); self.active = !self.active; self.copied_at = None; } - }; + } Task::none() } @@ -395,7 +386,8 @@ where text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) - .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor)) + .on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished)) + // .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused .leading_icon( color_button( None, From 24464908f6503e0f7923357a19a578151f18f50a Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 2 Apr 2026 18:15:41 -0600 Subject: [PATCH 131/168] fix: buttons are focusable again --- src/widget/button/widget.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index a4e32378..4acf3f2d 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -357,6 +357,8 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); + let state = tree.state.downcast_mut::(); + operation.focusable(Some(&self.id), layout.bounds(), state); } fn update( From 97a805e5a184c122364e47d7eceac763987fb491 Mon Sep 17 00:00:00 2001 From: Hendrik Hamerlinck Date: Wed, 11 Feb 2026 22:34:22 +0100 Subject: [PATCH 132/168] feat(applets): add destroy tooltip popup action This commit adds a new surface action to explicitly destroy the tooltip popup on `TOOLTIP_WINDOW_ID`, allowing proper cleanup when minimizing applets. --- src/app/cosmic.rs | 11 +++++++++++ src/applet/mod.rs | 2 +- src/surface/mod.rs | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index b732eee9..030ed041 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -230,6 +230,17 @@ where iced_winit::commands::popup::destroy_popup(id) } #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::DestroyTooltipPopup => { + #[cfg(feature = "applet")] + { + iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID) + } + #[cfg(not(feature = "applet"))] + { + Task::none() + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } diff --git a/src/applet/mod.rs b/src/applet/mod.rs index a3f5228b..a7fc4069 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -42,7 +42,7 @@ static AUTOSIZE_ID: LazyLock = static AUTOSIZE_MAIN_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); static TOOLTIP_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("subsurface")); -static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); +pub(crate) static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); #[derive(Debug, Clone)] pub struct Context { diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 4598ac7c..0dad6459 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -36,6 +36,8 @@ pub enum Action { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), + /// Destroys the global tooltip popup subsurface + DestroyTooltipPopup, /// Create a window with a view function accepting the App as a parameter AppWindow( @@ -85,6 +87,7 @@ impl std::fmt::Debug for Action { } Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), + Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::ResponsiveMenuBar { menu_bar, limits, From b0f4e931f2c1d7d30e4fd0dee8f5b45b9b5038f9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Apr 2026 08:25:01 -0400 Subject: [PATCH 133/168] fix: font issues some fonts are not falling back when a glyph is missing for a selected font and weight --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 35d048ee..bdbc141b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ name = "cosmic" [features] default = [ + "advanced-shaping", "winit", "tokio", "a11y", @@ -16,7 +17,8 @@ default = [ "x11", "iced-wayland", "multi-window", -] # default = ["dbus-config", "multi-window", "a11y"] +] +advanced-shaping = ["iced/advanced-shaping"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget From cdd825b953b528b19907e185957f8bc203514d3d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Apr 2026 08:25:12 -0400 Subject: [PATCH 134/168] fix: update iced softbuffer released version doesn't support transparency yet --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 42e3afb5..2d4ede15 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 42e3afb5686eff08c78c9292bb83c36d5c8f5146 +Subproject commit 2d4ede1597860db0bfaccfbc0166ee89ac353fc2 From 34219d1fd4171fb14ea7b4f6f572f9cbd4952150 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Apr 2026 14:12:58 -0400 Subject: [PATCH 135/168] chore: wgpu cctk feature for wayland --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index bdbc141b..78922132 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ wayland = [ "iced-wayland", "iced_runtime/cctk", "iced_winit/cctk", + "iced_wgpu/cctk", "iced/cctk", "dep:cctk", ] From a9e0671075093ef417a9bae8d0ec39ac44a8c035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:58:26 +0200 Subject: [PATCH 136/168] fix(segmented_button): hover text style --- src/widget/segmented_button/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 76c74f3b..203fbc2e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -218,7 +218,7 @@ where maximum_button_width: u16::MAX, indent_spacing: 16, font_active: crate::font::semibold(), - font_hovered: crate::font::semibold(), + font_hovered: crate::font::default(), font_inactive: crate::font::default(), font_size: 14.0, height: Length::Shrink, From fdf3369cea2f772aabb5f7c4e5cdf6406780f6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:24:53 +0200 Subject: [PATCH 137/168] chore: re-export iced row and column This removes the custom row and column implementations and uses the iced ones directly. --- examples/about/src/main.rs | 2 +- examples/application/src/main.rs | 4 +- examples/calendar/src/main.rs | 6 +-- examples/image-button/src/main.rs | 2 +- examples/subscriptions/src/main.rs | 2 +- examples/text-input/src/main.rs | 4 +- src/ext.rs | 66 ------------------------ src/widget/about.rs | 56 +++++++++++--------- src/widget/button/icon.rs | 7 +-- src/widget/calendar.rs | 2 +- src/widget/context_menu.rs | 2 +- src/widget/header_bar.rs | 74 ++++++++++++--------------- src/widget/list/column.rs | 2 +- src/widget/mod.rs | 65 +++-------------------- src/widget/segmented_button/widget.rs | 6 +-- src/widget/settings/item.rs | 12 ++--- src/widget/settings/mod.rs | 4 +- src/widget/table/widget/compact.rs | 6 +-- src/widget/table/widget/standard.rs | 4 +- src/widget/toaster/mod.rs | 4 +- 20 files changed, 103 insertions(+), 227 deletions(-) diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index 50f25da4..c25a9b9a 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -132,7 +132,7 @@ impl cosmic::Application for App { fn view(&self) -> Element<'_, Self::Message> { let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); let centered = cosmic::widget::container( - widget::column() + widget::column::with_capacity(1) .push(show_about_button) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 831a47f1..53f1c28e 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,7 +54,7 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); @@ -190,7 +190,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column() + widget::column::with_capacity(5) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 240684c6..494087d1 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -85,8 +85,6 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column().spacing(12); - let calendar = cosmic::widget::calendar( &self.calendar_model, |date| Message::DateSelected(date), @@ -95,9 +93,7 @@ impl cosmic::Application for App { Weekday::Sunday, ); - content = content.push(calendar); - - let centered = cosmic::widget::container(content) + let centered = cosmic::widget::container(calendar) .width(iced::Length::Fill) .height(iced::Length::Shrink) .align_x(iced::Alignment::Center) diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index 0ac906ca..c68c7070 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -80,7 +80,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column().spacing(12); + let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12); for (id, image) in self.images.iter().enumerate() { content = content.push( diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs index 47bd3772..17e630aa 100644 --- a/examples/subscriptions/src/main.rs +++ b/examples/subscriptions/src/main.rs @@ -64,7 +64,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - widget::row().into() + widget::Row::new().into() } } diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs index ea99666c..c17fcd5c 100644 --- a/examples/text-input/src/main.rs +++ b/examples/text-input/src/main.rs @@ -99,7 +99,9 @@ impl cosmic::Application for App { let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); - let column = cosmic::widget::column().push(editable).push(inline); + let column = cosmic::widget::column::with_capacity(2) + .push(editable) + .push(inline); let centered = cosmic::widget::container(column.width(200)) .width(iced::Length::Fill) diff --git a/src/ext.rs b/src/ext.rs index c85e6e86..8eb749e5 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -19,72 +19,6 @@ impl ElementExt for crate::Element<'_, Message> { } } -/// Additional methods for the [`Column`] and [`Row`] widgets. -pub trait CollectionWidget<'a, Message: 'a>: - Widget -where - Self: Sized, -{ - /// Moves all the elements of `other` into `self`, leaving `other` empty. - #[must_use] - fn append(self, other: &mut Vec) -> Self - where - E: Into>; - - /// Appends all elements in an iterator to the widget. - #[must_use] - fn extend(mut self, iterator: impl Iterator) -> Self - where - E: Into>, - { - for item in iterator { - self = self.push(item.into()); - } - - self - } - - /// Pushes an element into the widget. - #[must_use] - fn push(self, element: impl Into>) -> Self; - - /// Conditionally pushes an element to the widget. - #[must_use] - fn push_maybe(self, element: Option>>) -> Self { - if let Some(element) = element { - self.push(element.into()) - } else { - self - } - } -} - -impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Column<'a, Message> { - fn append(self, other: &mut Vec) -> Self - where - E: Into>, - { - self.extend(other.drain(..).map(Into::into)) - } - - fn push(self, element: impl Into>) -> Self { - self.push(element) - } -} - -impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, Message> { - fn append(self, other: &mut Vec) -> Self - where - E: Into>, - { - self.extend(other.drain(..).map(Into::into)) - } - - fn push(self, element: impl Into>) -> Self { - self.push(element) - } -} - pub trait ColorExt { /// Combines color with background to create appearance of transparency. #[must_use] diff --git a/src/widget/about.rs b/src/widget/about.rs index ba88e03a..148af02a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -47,32 +47,40 @@ pub struct About { fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { contributors .into_iter() - .map(|(name, email)| (name.to_string(), format!("mailto:{email}"))) + .map(|(name, email)| (name.into(), format!("mailto:{email}"))) .collect() } -macro_rules! set_contributors { - ($field:ident, $doc:expr) => { - #[doc = $doc] - pub fn $field(mut self, contributors: impl Into>) -> Self { - self.$field = add_contributors(contributors.into()); - self - } - }; -} - impl<'a> About { - set_contributors!(artists, "Artists who contributed to the application."); - set_contributors!(designers, "Designers who contributed to the application."); - set_contributors!(developers, "Developers who contributed to the application."); - set_contributors!( - documenters, - "Documenters who contributed to the application." - ); - set_contributors!( - translators, - "Translators who contributed to the application." - ); + /// Artists who contributed to the application. + pub fn artists(mut self, contributors: impl Into>) -> Self { + self.artists = add_contributors(contributors.into()); + self + } + + /// Designers who contributed to the application. + pub fn designers(mut self, contributors: impl Into>) -> Self { + self.designers = add_contributors(contributors.into()); + self + } + + /// Developers who contributed to the application. + pub fn developers(mut self, contributors: impl Into>) -> Self { + self.developers = add_contributors(contributors.into()); + self + } + + /// Documenters who contributed to the application. + pub fn documenters(mut self, contributors: impl Into>) -> Self { + self.documenters = add_contributors(contributors.into()); + self + } + + /// Translators who contributed to the application. + pub fn translators(mut self, contributors: impl Into>) -> Self { + self.translators = add_contributors(contributors.into()); + self + } /// Links associated with the application. pub fn links, V: Into>( @@ -97,7 +105,7 @@ pub fn about<'a, Message: Clone + 'static>( } = crate::theme::spacing(); let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { - widget::row() + widget::row::with_capacity(3) .push(widget::text(name)) .push(space::horizontal()) .push_maybe( @@ -158,7 +166,7 @@ pub fn about<'a, Message: Clone + 'static>( let copyright = about.copyright.as_ref().map(widget::text::body); let comments = about.comments.as_ref().map(widget::text::body); - widget::column() + widget::column::with_capacity(10) .push_maybe(header) .push_maybe(links_section) .push_maybe(developers_section) diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index edb54272..04d2bdd5 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -3,10 +3,7 @@ use super::{Builder, ButtonClass}; use crate::Element; -use crate::widget::{ - icon::{self, Handle}, - tooltip, -}; +use crate::widget::{icon::Handle, tooltip}; use apply::Apply; use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use std::borrow::Cow; @@ -133,7 +130,7 @@ impl Button<'_, Message> { } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { + fn from(builder: Button<'a, Message>) -> Element<'a, Message> { let mut content = Vec::with_capacity(2); content.push( diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 7c09d39c..19758472 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -212,7 +212,7 @@ where let content_list = column::with_children([ row::with_children([ - column().push(date).push(day).into(), + column([date.into(), day.into()]).into(), crate::widget::space::horizontal() .width(Length::Fill) .into(), diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 918d4da2..3f35f04a 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -32,7 +32,7 @@ pub fn context_menu<'a, Message: 'static + Clone>( content: content.into(), context_menu: context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::row::<'static, Message>()), + crate::Element::from(crate::widget::Row::new()), menus, )] }), diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1c0ca2c0..a772f7d2 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -243,10 +243,13 @@ impl<'a, Message: Clone + 'static> Widget Widget, viewport: &iced_core::Rectangle, ) { - for ((e, s), l) in self - .elems_mut() + self.elems_mut() .zip(&mut state.children) .zip(layout.children()) - { - e.as_widget_mut() - .update(s, event, l, cursor, renderer, clipboard, shell, viewport); - } + .for_each(|((e, s), l)| { + e.as_widget_mut() + .update(s, event, l, cursor, renderer, clipboard, shell, viewport); + }); } fn mouse_interaction( @@ -296,13 +298,12 @@ impl<'a, Message: Clone + 'static> Widget, ) { - for ((e, s), l) in self - .elems_mut() + self.elems_mut() .zip(&mut state.children) .zip(layout.children()) - { - e.as_widget_mut().operate(s, l, renderer, operation); - } + .for_each(|((e, s), l)| { + e.as_widget_mut().operate(s, l, renderer, operation); + }); } fn overlay<'b>( @@ -313,27 +314,13 @@ impl<'a, Message: Clone + 'static> Widget Option> { - let mut layouts = layout.children(); - let mut try_overlay = |elem: &'b mut Element<'a, Message>, - state: &'b mut tree::Tree| - -> Option< - iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>, - > { - elem.as_widget_mut() - .overlay(state, layouts.next()?, renderer, viewport, translation) - }; - - if let Some(center) = &mut self.center { - let (start_slice, end_center) = state.children.split_at_mut(1); - let (end_slice, center_slice) = end_center.split_at_mut(1); - try_overlay(&mut self.start, &mut start_slice[0]) - .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) - .or_else(|| try_overlay(center, &mut center_slice[0])) - } else { - let (start_slice, end_slice) = state.children.split_at_mut(1); - try_overlay(&mut self.start, &mut start_slice[0]) - .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) - } + self.elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + .find_map(|((e, s), l)| { + e.as_widget_mut() + .overlay(s, l, renderer, viewport, translation) + }) } fn drag_destinations( @@ -343,10 +330,13 @@ impl<'a, Message: Clone + 'static> Widget HeaderBar<'a, Message> { let mut widget = HeaderBarWidget::new(start, center, end) .apply(widget::container) - .class(crate::theme::Container::HeaderBar { + .class(theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, @@ -463,7 +453,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { widget::icon::from_name($name) .apply(widget::button::icon) .padding(8) - .class(crate::theme::Button::HeaderBar) + .class(theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 136b49ea..945b9140 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -63,7 +63,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { } // Ensure a minimum height of 32. - let list_item = iced::widget::row![ + let list_item = crate::widget::row![ container(item).align_y(iced::Alignment::Center), vertical().height(iced::Length::Fixed(32.)) ] diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 0f607240..ef212dab 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -24,7 +24,7 @@ //! .on_press(Message::LaunchUrl(REPOSITORY)) //! .padding(0); //! -//! let content = widget::column() +//! let content = widget::column::with_capacity(3) //! .push(widget::icon::from_name("my-app-icon")) //! .push(widget::text::title3("My App Name")) //! .push(link) @@ -53,6 +53,9 @@ pub use iced::widget::{Canvas, canvas}; #[doc(inline)] pub use iced::widget::{Checkbox, checkbox}; +#[doc(inline)] +pub use iced::widget::{Column, column}; + #[doc(inline)] pub use iced::widget::{ComboBox, combo_box}; @@ -80,6 +83,9 @@ pub use iced::widget::{ProgressBar, progress_bar}; #[doc(inline)] pub use iced::widget::{Responsive, responsive}; +#[doc(inline)] +pub use iced::widget::{Row, row}; + #[doc(inline)] pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; @@ -135,34 +141,6 @@ pub mod context_drawer; #[doc(inline)] pub use context_drawer::{ContextDrawer, context_drawer}; -#[doc(inline)] -pub use column::{Column, column}; -pub mod column { - //! A container which aligns its children in a column. - - pub type Column<'a, Message> = iced::widget::Column<'a, Message, crate::Theme, crate::Renderer>; - - #[must_use] - /// A container which aligns its children in a column. - pub fn column<'a, Message>() -> Column<'a, Message> { - Column::new() - } - - #[must_use] - /// A pre-allocated [`column`]. - pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> { - Column::with_capacity(capacity) - } - - #[must_use] - /// A [`column`] that will be assigned an [`Iterator`] of children. - pub fn with_children<'a, Message>( - children: impl IntoIterator>, - ) -> Column<'a, Message> { - Column::with_children(children) - } -} - pub mod layer_container; #[doc(inline)] pub use layer_container::{LayerContainer, layer_container}; @@ -287,35 +265,6 @@ pub mod rectangle_tracker; #[doc(inline)] pub use rectangle_tracker::{RectangleTracker, rectangle_tracking_container}; -#[doc(inline)] -pub use row::{Row, row}; - -pub mod row { - //! A container which aligns its children in a row. - - pub type Row<'a, Message> = iced::widget::Row<'a, Message, crate::Theme, crate::Renderer>; - - #[must_use] - /// A container which aligns its children in a row. - pub fn row<'a, Message>() -> Row<'a, Message> { - Row::new() - } - - #[must_use] - /// A pre-allocated [`row`]. - pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> { - Row::with_capacity(capacity) - } - - #[must_use] - /// A [`row`] that will be assigned an [`Iterator`] of children. - pub fn with_children<'a, Message>( - children: impl IntoIterator>, - ) -> Row<'a, Message> { - Row::with_children(children) - } -} - pub mod scrollable; #[doc(inline)] pub use scrollable::scrollable; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 203fbc2e..b9d1000e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -305,7 +305,7 @@ where { self.context_menu = context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::row::<'static, Message>()), + crate::Element::from(crate::widget::Row::new()), menus, )] }); @@ -1481,7 +1481,7 @@ where } } } else { - if let Item::Tab(key) = std::mem::replace(&mut state.hovered, Item::None) { + if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) { for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } @@ -2139,7 +2139,7 @@ where tree: &'b mut Tree, layout: iced_core::Layout<'b>, _renderer: &Renderer, - viewport: &iced_core::Rectangle, + _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 110ab7b7..349d93d8 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -4,7 +4,7 @@ use std::borrow::Cow; use crate::{ - Element, theme, + Element, Theme, theme, widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; @@ -18,12 +18,12 @@ use taffy::AlignContent; pub fn item<'a, Message: 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, -) -> Row<'a, Message> { +) -> Row<'a, Message, Theme> { #[inline(never)] fn inner<'a, Message: 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, - ) -> Row<'a, Message> { + ) -> Row<'a, Message, Theme> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), space::horizontal().into(), @@ -37,7 +37,7 @@ pub fn item<'a, Message: 'static>( /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item_row(children: Vec>) -> Row { +pub fn item_row(children: Vec>) -> Row { row::with_children(children) .spacing(theme::spacing().space_xs) .align_y(iced::Alignment::Center) @@ -105,7 +105,7 @@ pub struct Item<'a, Message> { impl<'a, Message: 'static> Item<'a, Message> { /// Assigns a control to the item. - pub fn control(self, widget: impl Into>) -> Row<'a, Message> { + pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { item_row(self.control_(widget.into())) } @@ -142,7 +142,7 @@ impl<'a, Message: 'static> Item<'a, Message> { self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> Row<'a, Message> { + ) -> Row<'a, Message, Theme> { self.control( crate::widget::toggler(is_checked) .width(Length::Shrink) diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs index 597d9bdd..79d81697 100644 --- a/src/widget/settings/mod.rs +++ b/src/widget/settings/mod.rs @@ -8,10 +8,10 @@ pub use self::item::{flex_item, flex_item_row, item, item_row}; pub use self::section::{Section, section}; use crate::widget::{Column, column}; -use crate::{Element, theme}; +use crate::{Element, Theme, theme}; /// A column with a predefined style for creating a settings panel #[must_use] -pub fn view_column(children: Vec>) -> Column { +pub fn view_column(children: Vec>) -> Column { column::with_children(children).spacing(theme::spacing().space_m) } diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index db71a1af..65ac9058 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -65,7 +65,7 @@ where let selected = val.model.is_active(entity); let context_menu = (val.item_context_builder)(item); - widget::column() + widget::column::with_capacity(2) .spacing(val.item_spacing) .push( widget::divider::horizontal::default() @@ -73,7 +73,7 @@ where .padding(val.divider_padding), ) .push( - widget::row() + widget::row::with_capacity(2) .spacing(space_xxxs) .align_y(Alignment::Center) .push_maybe( @@ -81,7 +81,7 @@ where .map(|icon| icon.size(val.icon_size)), ) .push( - widget::column() + widget::column::with_capacity(2) .push(widget::text::body(item.get_text(Category::default()))) .push({ let mut elements = val diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 1fa611f3..9ab76c9d 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -99,7 +99,7 @@ where }; // Build the category header - widget::row() + widget::row::with_capacity(2) .spacing(val.icon_spacing) .push(widget::text::heading(category.to_string())) .push_maybe(match sort_state { @@ -152,7 +152,7 @@ where categories .iter() .map(|category| { - widget::row() + widget::row::with_capacity(2) .spacing(val.icon_spacing) .push_maybe( item.get_icon(*category) diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index efd93a9d..bafaa9f9 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -34,10 +34,10 @@ pub fn toaster<'a, Message: Clone + 'static>( } = theme.cosmic().spacing; let make_toast = move |(id, toast): (ToastId, &'a Toast)| { - let row = row() + let row = row::with_capacity(2) .push(text(&toast.message)) .push( - row() + row::with_capacity(2) .push_maybe(toast.action.as_ref().map(|action| { button::text(&action.description).on_press((action.message)(id)) })) From 1d01054993862f615adb029379307cd5501c79f7 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 3 Apr 2026 17:05:24 -0600 Subject: [PATCH 138/168] chore: update iced pulls in fixes for cycling focus --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2d4ede15..ed9ad80e 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2d4ede1597860db0bfaccfbc0166ee89ac353fc2 +Subproject commit ed9ad80e18fdaa442a60f9cfce5b8841e19e9ef3 From 8e3672a7dd6aa2fb8d663b1379fa80afdd1ab75b Mon Sep 17 00:00:00 2001 From: KENZ Date: Sun, 5 Apr 2026 14:33:23 +0900 Subject: [PATCH 139/168] fix: focus detecting in IME logic --- src/widget/text_input/input.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index cd93a7d7..12fd731b 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -2095,7 +2095,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } input_method::Event::Preedit(content, selection) => { - if state.is_focused.is_some() { + if state.is_focused() { state.preedit = Some(input_method::Preedit { content: content.to_owned(), selection: selection.clone(), @@ -2106,7 +2106,7 @@ pub fn update<'a, Message: Clone + 'static>( } } input_method::Event::Commit(text) => { - let Some(focus) = &mut state.is_focused else { + let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else { return; }; let Some(on_input) = on_input else { @@ -2337,8 +2337,7 @@ fn input_method<'b>( text_layout: Layout<'_>, value: &Value, ) -> InputMethod<&'b str> { - if state.is_focused() { - } else { + if !state.is_focused() { return InputMethod::Disabled; }; From ab3eedd0f2e2ed7de9108ba6728261d8bae9e48d Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 6 Apr 2026 09:40:43 -0600 Subject: [PATCH 140/168] chore: update iced This pulls in the fix in cosmic-text to fallback to the default SansSerif if there are missing glyphs in basic shaping. Also removes advanced-shaping from the default features list. --- Cargo.toml | 1 - iced | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78922132..83fe90f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ name = "cosmic" [features] default = [ - "advanced-shaping", "winit", "tokio", "a11y", diff --git a/iced b/iced index ed9ad80e..7fd263d9 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ed9ad80e18fdaa442a60f9cfce5b8841e19e9ef3 +Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 From 9aa87cd66b94b3d7d4dc2047e9c85b93f968d1d0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 6 Apr 2026 18:16:32 -0400 Subject: [PATCH 141/168] fix(segmented_button): active font for context menu & prioritize active font over hover --- src/widget/segmented_button/widget.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index b9d1000e..a2efdfb8 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -246,12 +246,13 @@ where fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) { + let font = if self.button_is_focused(state, key) + || state.show_context == Some(key) + || self.model.is_active(key) + { self.font_active - } else if state.show_context == Some(key) || self.button_is_hovered(state, key) { + } else if self.button_is_hovered(state, key) { self.font_hovered - } else if self.model.is_active(key) { - self.font_active } else { self.font_inactive }; From 1f87cbc88320e540db408d65b763a4bed675b93e Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 6 Apr 2026 23:04:49 -0600 Subject: [PATCH 142/168] fix: do not allow cursor or keyboard activity when popup is open traps Tab from escaping, and won't allow elements in the background to react to hover --- src/widget/popover.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 7a82cd86..af5370a8 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -138,6 +138,10 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { + // Skip operating on background content, prevents Tab from escaping + if self.modal && self.popup.is_some() { + return; + } self.content .as_widget_mut() .operate(content_tree_mut(tree), layout, renderer, operation); @@ -172,11 +176,17 @@ where } } + // Hide cursor from background content when modal popup is active + let cursor = if self.modal && self.popup.is_some() { + mouse::Cursor::Unavailable + } else { + cursor_position + }; self.content.as_widget_mut().update( &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -214,13 +224,19 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { + // Hide cursor from background content when a modal popup is active + let cursor = if self.modal && self.popup.is_some() { + mouse::Cursor::Unavailable + } else { + cursor_position + }; self.content.as_widget().draw( content_tree(tree), renderer, theme, renderer_style, layout, - cursor_position, + cursor, viewport, ); } From 724351727a191516ca1b2f2f90a00b7d211c7e1f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 6 Apr 2026 22:56:18 -0600 Subject: [PATCH 143/168] feat: select until char and double click select delimiter adds a feature to select from the start of the sentence until the last occurrence of a character. This can be used to select until the extension in cosmic-files save dialog or rename pop up. Also, it adds a feature to select until the last occurrence of a character on double-click. --- src/widget/text_input/input.rs | 45 +++++++++++++++++++++++++++++++--- src/widget/text_input/value.rs | 8 ++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 12fd731b..806ceda0 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -188,6 +188,7 @@ pub struct TextInput<'a, Message> { is_editable_variant: bool, is_read_only: bool, select_on_focus: bool, + double_click_select_delimiter: Option, font: Option<::Font>, width: Length, padding: Padding, @@ -238,6 +239,7 @@ where is_editable_variant: false, is_read_only: false, select_on_focus: false, + double_click_select_delimiter: None, font: None, width: Length::Fill, padding: spacing.into(), @@ -343,6 +345,17 @@ where self } + /// Sets a delimiter character for double-click selection behavior. + /// + /// When set, double-clicking before the last occurrence of this character + /// selects from the start to that character. Double-clicking after the + /// delimiter uses normal word selection. + #[inline] + pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self { + self.double_click_select_delimiter = Some(delimiter); + self + } + /// Emits a message when an unfocused text input has been focused by click. /// /// This will not trigger if the input was focused externally by the application. @@ -598,6 +611,7 @@ where self.value = state.tracked_value.clone(); // std::mem::swap(&mut state.tracked_value, &mut self.value); } + state.double_click_select_delimiter = self.double_click_select_delimiter; // Unfocus text input if it becomes disabled if self.on_input.is_none() && !self.manage_value { state.last_click = None; @@ -1180,6 +1194,14 @@ pub fn select_range(id: Id, start: usize, end: usize) -> Task< ))) } +/// Produces a [`Task`] that selects from the front to the last occurrence of the given character +/// in the [`TextInput`] with the given [`Id`], or selects all if not found. +pub fn select_until_last(id: Id, value: &str, ch: char) -> Task { + let v = Value::new(value); + let end = v.rfind_char(ch).unwrap_or(v.len()); + select_range(id, 0, end) +} + /// Computes the layout of a [`TextInput`]. #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] @@ -1600,10 +1622,23 @@ pub fn update<'a, Message: Clone + 'static>( .unwrap_or((0, text::Affinity::Before)); state.cursor.set_affinity(affinity); - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); + + if let Some(delimiter) = state.double_click_select_delimiter { + if let Some(delim_pos) = value.rfind_char(delimiter) { + if position <= delim_pos { + state.cursor.select_range(0, delim_pos); + } else { + state.cursor.select_range(delim_pos + 1, value.len()); + } + } else { + state.cursor.select_all(value); + } + } else { + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); + } } state.dragging_state = Some(DraggingState::Selection); } @@ -2882,6 +2917,7 @@ pub struct State { pub is_read_only: bool, pub emit_unfocus: bool, select_on_focus: bool, + double_click_select_delimiter: Option, is_focused: Option, dragging_state: Option, dnd_offer: DndOfferState, @@ -2963,6 +2999,7 @@ impl State { emit_unfocus: false, is_focused: None, select_on_focus: false, + double_click_select_delimiter: None, dragging_state: None, dnd_offer: DndOfferState::default(), is_pasting: None, diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 9faff4ac..3f7b8d73 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -142,6 +142,14 @@ impl Value { .sum() } + /// Returns the grapheme index of the last occurrence of the given character, + /// searching from the end. + #[must_use] + pub fn rfind_char(&self, ch: char) -> Option { + let needle = ch.to_string(); + self.graphemes.iter().rposition(|g| g == &needle) + } + /// Converts a byte index to a grapheme index. #[must_use] pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { From b963fbfea9a94316dd1a0d99e84a5116cf696853 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:02:58 -0400 Subject: [PATCH 144/168] feat(widget): progress bars --- examples/application/src/main.rs | 50 +++ src/widget/mod.rs | 9 +- src/widget/progress_bar/circular.rs | 453 ++++++++++++++++++++++++++ src/widget/progress_bar/linear.rs | 306 +++++++++++++++++ src/widget/progress_bar/mod.rs | 11 + src/widget/progress_bar/style.rs | 105 ++++++ src/widget/segmented_button/widget.rs | 9 +- 7 files changed, 935 insertions(+), 8 deletions(-) create mode 100644 src/widget/progress_bar/circular.rs create mode 100644 src/widget/progress_bar/linear.rs create mode 100644 src/widget/progress_bar/mod.rs create mode 100644 src/widget/progress_bar/style.rs diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 53f1c28e..bceece6e 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -82,6 +82,7 @@ pub enum Message { Hi, Hi2, Hi3, + Tick, } /// The [`App`] stores application-specific state. @@ -92,6 +93,7 @@ pub struct App { input_2: String, hidden: bool, keybinds: HashMap, + progress: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -133,6 +135,7 @@ impl cosmic::Application for App { input_2: String::new(), hidden: true, keybinds: HashMap::new(), + progress: 0.0, }; let command = app.update_title(); @@ -178,10 +181,17 @@ impl cosmic::Application for App { Message::Hi3 => { dbg!("hi 3"); } + Message::Tick => { + self.progress = (self.progress + 0.01) % 1.0; + } } Task::none() } + fn subscription(&self) -> iced::Subscription { + iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick) + } + /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let page_content = self @@ -212,6 +222,46 @@ impl cosmic::Application for App { .on_input(Message::Input2) .on_clear(Message::Ignore), ) + .push(widget::progress_bar::circular::Circular::new().size(50.0)) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .width(Length::Fill), + ) + .push( + widget::progress_bar::circular::Circular::new() + .bar_height(10.0) + .size(50.0) + .progress(self.progress), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(self.progress) + .width(Length::Fill), + ) + .push( + widget::progress_bar::circular::Circular::new() + .size(50.0) + .progress(0.0), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(0.0) + .width(Length::Fill), + ) + .push( + widget::progress_bar::circular::Circular::new() + .size(50.0) + .progress(1.0), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(1.0) + .width(Length::Fill), + ) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index ef212dab..7dcfa233 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -77,9 +77,6 @@ pub use iced::widget::{MouseArea, mouse_area}; #[doc(inline)] pub use iced::widget::{PaneGrid, pane_grid}; -#[doc(inline)] -pub use iced::widget::{ProgressBar, progress_bar}; - #[doc(inline)] pub use iced::widget::{Responsive, responsive}; @@ -257,6 +254,12 @@ pub mod popover; #[doc(inline)] pub use popover::{Popover, popover}; +pub mod progress_bar; +#[doc(inline)] +pub use progress_bar::{ + circular, circular::Circular, circular_progress, linear, linear::Linear, linear_progress, style, +}; + pub mod radio; #[doc(inline)] pub use radio::{Radio, radio}; diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs new file mode 100644 index 00000000..7e8177d6 --- /dev/null +++ b/src/widget/progress_bar/circular.rs @@ -0,0 +1,453 @@ +//! Show a circular progress indicator. +use super::style::StyleSheet; +use crate::anim::smootherstep; +use iced::advanced::layout; +use iced::advanced::renderer; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; +use iced::mouse; +use iced::time::Instant; +use iced::widget::canvas; +use iced::window; +use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; + +use std::f32::consts::PI; +use std::time::Duration; + +const MIN_ANGLE: Radians = Radians(PI / 8.0); +const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0); +const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; + +#[must_use] +pub struct Circular +where + Theme: StyleSheet, +{ + size: f32, + bar_height: f32, + style: ::Style, + cycle_duration: Duration, + rotation_duration: Duration, + progress: Option, +} + +impl Circular +where + Theme: StyleSheet, +{ + /// Creates a new [`Circular`] with the given content. + pub fn new() -> Self { + Circular { + size: 40.0, + bar_height: 4.0, + style: ::Style::default(), + cycle_duration: Duration::from_millis(1500), + rotation_duration: Duration::from_secs(2), + progress: None, + } + } + + /// Sets the size of the [`Circular`]. + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + /// Sets the bar height of the [`Circular`]. + pub fn bar_height(mut self, bar_height: f32) -> Self { + self.bar_height = bar_height; + self + } + + /// Sets the style variant of this [`Circular`]. + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } + + /// Sets the cycle duration of this [`Circular`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } + + /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full + /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) + pub fn rotation_duration(mut self, duration: Duration) -> Self { + self.rotation_duration = duration; + self + } + + /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. + pub fn progress(mut self, progress: f32) -> Self { + self.progress = Some(progress.clamp(0.0, 1.0)); + self + } +} + +impl Default for Circular +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum Animation { + Expanding { + start: Instant, + progress: f32, + rotation: u32, + last: Instant, + }, + Contracting { + start: Instant, + progress: f32, + rotation: u32, + last: Instant, + }, +} + +impl Default for Animation { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + rotation: 0, + last: Instant::now(), + } + } +} + +impl Animation { + fn next(&self, additional_rotation: u32, now: Instant) -> Self { + match self { + Self::Expanding { rotation, .. } => Self::Contracting { + start: now, + progress: 0.0, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + Self::Contracting { rotation, .. } => Self::Expanding { + start: now, + progress: 0.0, + rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( + (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, + )), + last: now, + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, + } + } + + fn last(&self) -> Instant { + match self { + Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last, + } + } + + fn timed_transition( + &self, + cycle_duration: Duration, + rotation_duration: Duration, + now: Instant, + ) -> Self { + let elapsed = now.duration_since(self.start()); + let additional_rotation = ((now - self.last()).as_secs_f32() + / rotation_duration.as_secs_f32() + * (u32::MAX) as f32) as u32; + + match elapsed { + elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), + _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), + } + } + + fn with_elapsed( + &self, + cycle_duration: Duration, + additional_rotation: u32, + elapsed: Duration, + now: Instant, + ) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { + start, rotation, .. + } => Self::Expanding { + start: *start, + progress, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + Self::Contracting { + start, rotation, .. + } => Self::Contracting { + start: *start, + progress, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + } + } + + fn rotation(&self) -> f32 { + match self { + Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => { + *rotation as f32 / u32::MAX as f32 + } + } + } +} + +#[derive(Default)] +struct State { + animation: Animation, + cache: canvas::Cache, + progress: Option, +} + +impl Widget for Circular +where + Message: Clone, + Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> Size { + Size { + width: Length::Fixed(self.size), + height: Length::Fixed(self.size), + } + } + + fn layout( + &mut self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.size, self.size) + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_mut::(); + if self.progress.is_some() { + if !float_cmp::approx_eq!( + f32, + state.progress.unwrap_or_default(), + self.progress.unwrap_or_default() + ) { + state.progress = self.progress; + state.cache.clear(); + } + return; + } + if let Event::Window(window::Event::RedrawRequested(now)) = event { + state.animation = + state + .animation + .timed_transition(self.cycle_duration, self.rotation_duration, *now); + + state.cache.clear(); + shell.request_redraw(); + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + use advanced::Renderer as _; + + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let custom_style = + ::appearance(theme, &self.style, self.progress.is_some(), true); + + let geometry = state.cache.draw(renderer, bounds.size(), |frame| { + let track_radius = frame.width() / 2.0 - self.bar_height; + let track_path = canvas::Path::circle(frame.center(), track_radius); + + frame.stroke( + &track_path, + canvas::Stroke::default() + .with_color(custom_style.track_color) + .with_width(self.bar_height), + ); + + if let Some(progress) = self.progress { + // outer border + if let Some(border_color) = custom_style.border_color { + let border_path = + canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0); + + frame.stroke( + &border_path, + canvas::Stroke::default() + .with_color(border_color) + .with_width(1.0), + ); + } + + // inner border + if let Some(border_color) = custom_style.border_color { + let border_path = + canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0); + + frame.stroke( + &border_path, + canvas::Stroke::default() + .with_color(border_color) + .with_width(1.0), + ); + } + + // bar + let mut builder = canvas::path::Builder::new(); + + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: Radians(-PI / 2.0), + end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), + }); + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + // get center of end of arc for rounded cap + let end_angle = -PI / 2.0 + progress * 2.0 * PI; + let end_center = + frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: end_center, + radius: self.bar_height / 2.0, + start_angle: Radians(end_angle), + end_angle: Radians(end_angle + PI), + }); + + // get center of start of arc for rounded cap + let start_angle = -PI / 2.0; + let start_center = frame.center() + + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: start_center, + radius: self.bar_height / 2.0, + start_angle: Radians(start_angle - PI), + end_angle: Radians(start_angle), + }); + + let cap_path = builder.build(); + frame.fill(&cap_path, custom_style.bar_color); + } else { + let mut builder = canvas::path::Builder::new(); + + let start = Radians(state.animation.rotation() * 2.0 * PI); + let (start_angle, end_angle) = match state.animation { + Animation::Expanding { progress, .. } => ( + start, + start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)), + ), + Animation::Contracting { progress, .. } => ( + start + WRAP_ANGLE * (smootherstep(progress)), + start + MIN_ANGLE + WRAP_ANGLE, + ), + }; + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle, + end_angle, + }); + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + // get center of end of arc for rounded cap + let end_center = frame.center() + + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: end_center, + radius: self.bar_height / 2.0, + start_angle: Radians(end_angle.0), + end_angle: Radians(end_angle.0 + PI), + }); + + // get center of start of arc for rounded cap + let start_center = frame.center() + + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: start_center, + radius: self.bar_height / 2.0, + start_angle: Radians(start_angle.0 - PI), + end_angle: Radians(start_angle.0), + }); + + let cap_path = builder.build(); + frame.fill(&cap_path, custom_style.bar_color); + } + }); + + renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| { + use iced::advanced::graphics::geometry::Renderer as _; + + renderer.draw_geometry(geometry); + }); + } +} + +impl<'a, Message, Theme> From> for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, +{ + fn from(circular: Circular) -> Self { + Self::new(circular) + } +} diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs new file mode 100644 index 00000000..226b2b5f --- /dev/null +++ b/src/widget/progress_bar/linear.rs @@ -0,0 +1,306 @@ +//! Show a linear progress indicator. +use iced::advanced::layout; +use iced::advanced::renderer::{self, Quad}; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; +use iced::mouse; +use iced::time::Instant; +use iced::window; +use iced::{Background, Element, Event, Length, Rectangle, Size}; + +use crate::anim::smootherstep; + +use super::style::StyleSheet; + +use std::time::Duration; + +#[must_use] +pub struct Linear +where + Theme: StyleSheet, +{ + width: Length, + girth: Length, + style: Theme::Style, + cycle_duration: Duration, + progress: Option, +} + +impl Linear +where + Theme: StyleSheet, +{ + /// Creates a new [`Linear`] with the given content. + pub fn new() -> Self { + Linear { + width: Length::Fixed(100.0), + girth: Length::Fixed(4.0), + style: Theme::Style::default(), + cycle_duration: Duration::from_millis(1500), + progress: None, + } + } + + /// Sets the width of the [`Linear`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the girth of the [`Linear`]. + pub fn girth(mut self, girth: impl Into) -> Self { + self.girth = girth.into(); + self + } + + /// Sets the style variant of this [`Linear`]. + pub fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); + self + } + + /// Sets the cycle duration of this [`Linear`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } + + /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. + pub fn progress(mut self, progress: f32) -> Self { + self.progress = Some(progress.clamp(0.0, 1.0)); + self + } +} + +impl Default for Linear +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum State { + Expanding { start: Instant, progress: f32 }, + Contracting { start: Instant, progress: f32 }, +} + +impl Default for State { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + } + } +} + +impl State { + fn next(&self, now: Instant) -> Self { + match self { + Self::Expanding { .. } => Self::Contracting { + start: now, + progress: 0.0, + }, + Self::Contracting { .. } => Self::Expanding { + start: now, + progress: 0.0, + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, + } + } + + fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { + let elapsed = now.duration_since(self.start()); + + match elapsed { + elapsed if elapsed > cycle_duration => self.next(now), + _ => self.with_elapsed(cycle_duration, elapsed), + } + } + + fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { start, .. } => Self::Expanding { + start: *start, + progress, + }, + Self::Contracting { start, .. } => Self::Contracting { + start: *start, + progress, + }, + } + } +} + +impl Widget for Linear +where + Message: Clone, + Theme: StyleSheet, + Renderer: advanced::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.girth, + } + } + + fn layout( + &mut self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.width, self.girth) + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + if self.progress.is_some() { + return; + } + + let state = tree.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition(self.cycle_duration, *now); + + shell.request_redraw(); + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); + let state = tree.state.downcast_ref::(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: if custom_style.border_color.is_some() { + 1.0 + } else { + 0.0 + }, + color: custom_style.border_color.unwrap_or(custom_style.bar_color), + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.track_color), + ); + + if let Some(progress) = self.progress { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: progress * bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: 0., + color: iced::Color::TRANSPARENT, + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.bar_color), + ); + } else { + match state { + State::Expanding { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: smootherstep(*progress) * bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: 0., + color: iced::Color::TRANSPARENT, + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.bar_color), + ), + + State::Contracting { progress, .. } => renderer.fill_quad( + Quad { + bounds: Rectangle { + x: bounds.x + smootherstep(*progress) * bounds.width, + y: bounds.y, + width: (1.0 - smootherstep(*progress)) * bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: 0., + color: iced::Color::TRANSPARENT, + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.bar_color), + ), + } + } + } +} + +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, + Renderer: iced::advanced::Renderer + 'a, +{ + fn from(linear: Linear) -> Self { + Self::new(linear) + } +} diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs new file mode 100644 index 00000000..c1230961 --- /dev/null +++ b/src/widget/progress_bar/mod.rs @@ -0,0 +1,11 @@ +pub mod circular; +pub mod linear; +pub mod style; + +pub fn circular_progress() -> circular::Circular { + circular::Circular::new() +} + +pub fn linear_progress() -> linear::Linear { + linear::Linear::new() +} diff --git a/src/widget/progress_bar/style.rs b/src/widget/progress_bar/style.rs new file mode 100644 index 00000000..db2fe64d --- /dev/null +++ b/src/widget/progress_bar/style.rs @@ -0,0 +1,105 @@ +use iced::Color; + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, + /// The border [`Color`] of the progress indicator. + pub border_color: Option, + /// The border radius of the progress indicator. + pub border_radius: f32, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + border_color: None, + border_radius: 0.0, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance( + &self, + style: &Self::Style, + is_determinate: bool, + is_circular: bool, + ) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance( + &self, + _style: &Self::Style, + _is_determinate: bool, + _is_circular: bool, + ) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + border_color: None, + border_radius: 0.0, + } + } +} + +impl StyleSheet for crate::Theme { + type Style = (); + + fn appearance( + &self, + _style: &Self::Style, + is_determinate: bool, + is_circular: bool, + ) -> Appearance { + let cur = self.current_container(); + let mut cur_divider = cur.divider; + cur_divider.alpha = 0.5; + let theme = self.cosmic(); + + let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast { + ( + theme.palette.neutral_6.into(), + theme.accent_text_color().into(), + ) + } else if theme.is_dark { + (theme.palette.neutral_5.into(), theme.accent_color().into()) + } else if theme.is_high_contrast { + ( + theme.palette.neutral_4.into(), + theme.accent_text_color().into(), + ) + } else { + (theme.palette.neutral_3.into(), theme.accent_color().into()) + }; + + if !is_determinate && is_circular { + track_color = Color::TRANSPARENT; + } + + Appearance { + track_color, + bar_color, + border_color: if is_determinate && theme.is_high_contrast { + Some(cur_divider.into()) + } else { + None + }, + border_radius: theme.corner_radii.radius_xl[0], + } + } +} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index a2efdfb8..5d862e9f 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -262,10 +262,10 @@ where font.hash(&mut hasher); let text_hash = hasher.finish(); - if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { - if prev_hash == text_hash { - return; - } + if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) + && prev_hash == text_hash + { + return; } if let Some(paragraph) = state.paragraphs.get_mut(key) { @@ -928,7 +928,6 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); - for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } From d9121d6f0dfff4116eea096459151d051befe1da Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 7 Apr 2026 15:37:13 -0400 Subject: [PATCH 145/168] refactor: better helpers for the progress_bar --- src/widget/mod.rs | 3 ++- src/widget/progress_bar/mod.rs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 7dcfa233..f442b0da 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -257,7 +257,8 @@ pub use popover::{Popover, popover}; pub mod progress_bar; #[doc(inline)] pub use progress_bar::{ - circular, circular::Circular, circular_progress, linear, linear::Linear, linear_progress, style, + circular, circular::Circular, determinate_circular, determinate_linear, indeterminate_circular, + indeterminate_linear, linear, linear::Linear, style, }; pub mod radio; diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index c1230961..ea069ffc 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -2,10 +2,22 @@ pub mod circular; pub mod linear; pub mod style; -pub fn circular_progress() -> circular::Circular { +/// A spinner / throbber widget that can be used to indicate that some operation is in progress. +pub fn indeterminate_circular() -> circular::Circular { circular::Circular::new() } -pub fn linear_progress() -> linear::Linear { +/// A linear throbber widget that can be used to indicate that some operation is in progress. +pub fn indeterminate_linear() -> linear::Linear { linear::Linear::new() } + +/// A circular progress spinner widget that can be used to indicate the progress of some operation. +pub fn determinate_circular(progress: f32) -> circular::Circular { + circular::Circular::new().progress(progress) +} + +/// A linear progress bar widget that can be used to indicate the progress of some operation. +pub fn determinate_linear(progress: f32) -> linear::Linear { + linear::Linear::new().progress(progress) +} From 5d1dfc4c54eba5d1037293fc0bc2ebfdc78eaab3 Mon Sep 17 00:00:00 2001 From: Adam Cosner <160804448+Adam-Cosner@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:12:10 +0000 Subject: [PATCH 146/168] refactor!: remove `cosmic::iced_*` re-exports --- examples/applet/src/window.rs | 4 +-- examples/context-menu/src/main.rs | 2 +- examples/menu/src/main.rs | 6 ++--- examples/multi-window/src/window.rs | 4 +-- examples/nav-context/src/main.rs | 2 +- examples/open-dialog/src/main.rs | 2 +- examples/table-view/src/main.rs | 2 +- src/applet/mod.rs | 16 ++++++------ src/applet/token/subscription.rs | 2 +- src/lib.rs | 23 ----------------- src/widget/calendar.rs | 2 +- src/widget/cards.rs | 9 +++---- src/widget/dnd_destination.rs | 29 ++++++++++----------- src/widget/dnd_source.rs | 18 +++++++------- src/widget/menu/menu_tree.rs | 2 +- src/widget/segmented_button/widget.rs | 6 ++--- src/widget/toggler.rs | 12 ++++----- src/widget/wrapper.rs | 36 +++++++++++++-------------- 18 files changed, 77 insertions(+), 100 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 4e05c70a..22903eac 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,8 +1,8 @@ use cosmic::app::{Core, Task}; +use cosmic::iced::core::window; use cosmic::iced::window::Id; use cosmic::iced::{Length, Rectangle}; -use cosmic::iced_runtime::core::window; use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::Element; @@ -159,7 +159,7 @@ impl cosmic::Application for Window { "oops".into() } - fn style(&self) -> Option { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index db66ba1b..e5ca5878 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -4,7 +4,7 @@ //! Application API example use cosmic::app::{Core, Settings, Task}; -use cosmic::iced_core::Size; +use cosmic::iced::Size; use cosmic::widget::menu; use cosmic::{executor, iced, ApplicationExt, Element}; use std::collections::HashMap; diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 8b5a1cb7..da0c3231 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -7,10 +7,10 @@ use std::collections::HashMap; use std::{env, process}; use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::alignment::{Horizontal, Vertical}; +use cosmic::iced::keyboard::Key; use cosmic::iced::window; -use cosmic::iced_core::alignment::{Horizontal, Vertical}; -use cosmic::iced_core::keyboard::Key; -use cosmic::iced_core::{Length, Size}; +use cosmic::iced::{Length, Size}; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::Modifier; diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 74ab5386..754a0d86 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use cosmic::{ app::Core, + iced::core::{id, Alignment, Length, Point}, + iced::widget::{column, container, scrollable, text}, iced::{self, event, window, Subscription}, - iced_core::{id, Alignment, Length, Point}, - iced_widget::{column, container, scrollable, text}, prelude::*, widget::{button, header_bar}, }; diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index fdfb90f9..1992066f 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced_core::Size; +use cosmic::iced::Size; use cosmic::widget::{menu, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 29061534..b4b5343f 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -6,7 +6,7 @@ use apply::Apply; use cosmic::app::{Core, Settings, Task}; use cosmic::dialog::file_chooser::{self, FileFilter}; -use cosmic::iced_core::Length; +use cosmic::iced::Length; use cosmic::widget::button; use cosmic::{executor, iced, ApplicationExt, Element}; use std::sync::Arc; diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index bbd9cf5b..d2478429 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::Datelike; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced_core::Size; +use cosmic::iced::Size; use cosmic::prelude::*; use cosmic::widget::table; use cosmic::widget::{self, nav_bar}; diff --git a/src/applet/mod.rs b/src/applet/mod.rs index a7fc4069..48721e1c 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -6,13 +6,6 @@ use crate::{ Application, Element, Renderer, app::iced_settings, cctk::sctk, - iced::{ - self, Color, Length, Limits, Rectangle, - alignment::{Alignment, Horizontal, Vertical}, - widget::Container, - window, - }, - iced_widget, theme::{self, Button, THEME, system_dark, system_light}, widget::{ self, @@ -24,8 +17,15 @@ use crate::{ space::vertical, }, }; + pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; +use iced::{ + self, Color, Length, Limits, Rectangle, + alignment::{Alignment, Horizontal, Vertical}, + widget::Container, + window, +}; use iced_core::{Padding, Shadow}; use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; @@ -226,7 +226,7 @@ impl Context { let symbolic = icon.symbolic; let icon = widget::icon(icon) .class(if symbolic { - theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { + theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { color: Some(theme.cosmic().background.on.into()), })) } else { diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 82763303..07c528ea 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,11 +1,11 @@ use crate::iced; -use crate::iced_futures::futures; use cctk::sctk::reexports::calloop; use futures::{ SinkExt, StreamExt, channel::mpsc::{UnboundedReceiver, unbounded}, }; use iced::Subscription; +use iced_futures::futures; use iced_futures::stream; use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; diff --git a/src/lib.rs b/src/lib.rs index aa3b7db2..e04f1609 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,29 +66,6 @@ pub mod font; #[doc(inline)] pub use iced; -#[doc(inline)] -pub use iced_core; - -#[doc(inline)] -pub use iced_futures; - -#[doc(inline)] -pub use iced_renderer; - -#[doc(inline)] -pub use iced_runtime; - -#[doc(inline)] -pub use iced_widget; - -#[doc(inline)] -#[cfg(feature = "winit")] -pub use iced_winit; - -#[doc(inline)] -#[cfg(feature = "wgpu")] -pub use iced_wgpu; - pub mod icon_theme; pub mod keyboard_nav; diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 19758472..91c601d3 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -4,10 +4,10 @@ //! A widget that displays an interactive calendar. use crate::fl; -use crate::iced_core::{Alignment, Length}; use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; use iced::alignment::Vertical; +use iced_core::{Alignment, Length}; use jiff::{ ToSpan, civil::{Date, Weekday}, diff --git a/src/widget/cards.rs b/src/widget/cards.rs index b8e17636..66267a73 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -1,13 +1,8 @@ //! An expandable stack of cards use std::time::Duration; -use self::iced_core::{ - Element, Event, Length, Size, Vector, Widget, border::Radius, id::Id, layout::Node, - renderer::Quad, widget::Tree, -}; use crate::{ anim, - iced_core::{self, Border, Shadow}, widget::{ button, card::style::Style, @@ -18,6 +13,10 @@ use crate::{ }; use float_cmp::approx_eq; use iced::widget; +use iced_core::{ + Border, Element, Event, Length, Shadow, Size, Vector, Widget, border::Radius, id::Id, + layout::Node, renderer::Quad, widget::Tree, +}; use iced_core::{widget::tree, window}; const ICON_SIZE: u16 = 16; diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index a77101b9..10bf7a8b 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -7,23 +7,24 @@ use iced::Vector; use crate::{ Element, - iced::{ - Event, Length, Rectangle, - clipboard::{ - dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, - mime::AllowedMimeTypes, - }, - event, - id::Internal, - mouse, overlay, - }, - iced_core::{ - self, Clipboard, Shell, layout, - widget::{Tree, tree}, - }, widget::{Id, Widget}, }; +use iced::{ + Event, Length, Rectangle, + clipboard::{ + dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, + mime::AllowedMimeTypes, + }, + event, + id::Internal, + mouse, overlay, +}; +use iced_core::{ + self, Clipboard, Shell, layout, + widget::{Tree, tree}, +}; + pub fn dnd_destination<'a, Message: 'static>( child: impl Into>, mimes: Vec>, diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 25900a66..980723e3 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -4,17 +4,17 @@ use iced_core::{widget::Operation, window}; use crate::{ Element, - iced::{ - Event, Length, Point, Rectangle, Vector, - clipboard::dnd::{DndAction, DndEvent, SourceEvent}, - event, mouse, overlay, - }, - iced_core::{ - self, Clipboard, Shell, layout, renderer, - widget::{Tree, tree}, - }, widget::{Id, Widget, container}, }; +use iced::{ + Event, Length, Point, Rectangle, Vector, + clipboard::dnd::{DndAction, DndEvent, SourceEvent}, + event, mouse, overlay, +}; +use iced_core::{ + self, Clipboard, Shell, layout, renderer, + widget::{Tree, tree}, +}; pub fn dnd_source< 'a, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 047df0ed..41cf1dff 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -9,11 +9,11 @@ use std::rc::Rc; use iced::advanced::widget::text::Style as TextStyle; use iced_widget::core::{Element, renderer}; -use crate::iced_core::{Alignment, Length}; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; use crate::widget::{Button, RcElementWrapper, icon}; use crate::{theme, widget}; +use iced_core::{Alignment, Length}; /// Nested menu is essentially a tree of items, a menu is a collection of items /// a menu itself can also be an item of another menu. diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 5d862e9f..44ca8574 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,7 +3,6 @@ use super::model::{Entity, Model, Selectable}; use super::{InsertPosition, ReorderEvent}; -use crate::iced_core::id::Internal; use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::dnd_destination::DragId; use crate::widget::menu::{ @@ -22,6 +21,7 @@ use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, keyboard, mouse, touch, window, }; +use iced_core::id::Internal; use iced_core::mouse::ScrollDelta; use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; @@ -2043,10 +2043,10 @@ where ..image_bounds }, crate::widget::icon(match crate::widget::common::object_select().data() { - crate::iced_core::svg::Data::Bytes(bytes) => { + iced_core::svg::Data::Bytes(bytes) => { crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) } - crate::iced_core::svg::Data::Path(path) => { + iced_core::svg::Data::Path(path) => { crate::widget::icon::from_path(path.clone()) } }), diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 9d31ca1e..05371a17 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -2,18 +2,18 @@ use std::time::{Duration, Instant}; -use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status}; +use crate::{Element, anim}; use iced_core::{ - Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, - layout, mouse, + Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, + event, layout, mouse, renderer::{self, Renderer}, text, touch, widget::{self, Tree, tree}, window, }; -use iced_widget::Id; +use iced_widget::{Id, toggler::Status}; -pub use crate::iced_widget::toggler::{Catalog, Style}; +pub use iced_widget::toggler::{Catalog, Style}; pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { Toggler::new(is_checked) @@ -200,7 +200,7 @@ impl<'a, Message> Widget for Toggler<'a, align_x: self.text_alignment, align_y: alignment::Vertical::Top, shaping: self.text_shaping, - wrapping: crate::iced_core::text::Wrapping::default(), + wrapping: iced_core::text::Wrapping::default(), ellipsize: self.ellipsize, }, ); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 73e476fa..133f9b87 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -93,8 +93,8 @@ impl Widget for RcElementWrapper { &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &crate::iced_core::layout::Limits, - ) -> crate::iced_core::layout::Node { + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { self.element .with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits)) } @@ -104,9 +104,9 @@ impl Widget for RcElementWrapper { tree: &tree::Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - style: &crate::iced_core::renderer::Style, - layout: crate::iced_core::Layout<'_>, - cursor: crate::iced_core::mouse::Cursor, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, viewport: &Rectangle, ) { self.element.with_data(move |e| { @@ -134,7 +134,7 @@ impl Widget for RcElementWrapper { fn operate( &mut self, state: &mut tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { @@ -148,11 +148,11 @@ impl Widget for RcElementWrapper { &mut self, state: &mut tree::Tree, event: &crate::iced::Event, - layout: crate::iced_core::Layout<'_>, - cursor: crate::iced_core::mouse::Cursor, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, - clipboard: &mut dyn crate::iced_core::Clipboard, - shell: &mut crate::iced_core::Shell<'_, M>, + clipboard: &mut dyn iced_core::Clipboard, + shell: &mut iced_core::Shell<'_, M>, viewport: &Rectangle, ) { self.element.with_data_mut(|e| { @@ -165,11 +165,11 @@ impl Widget for RcElementWrapper { fn mouse_interaction( &self, state: &tree::Tree, - layout: crate::iced_core::Layout<'_>, - cursor: crate::iced_core::mouse::Cursor, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, viewport: &Rectangle, renderer: &crate::Renderer, - ) -> crate::iced_core::mouse::Interaction { + ) -> iced_core::mouse::Interaction { self.element.with_data(|e| { e.as_widget() .mouse_interaction(state, layout, cursor, viewport, renderer) @@ -179,11 +179,11 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: crate::iced_core::Layout<'a>, + layout: iced_core::Layout<'a>, renderer: &crate::Renderer, viewport: &Rectangle, - translation: crate::iced_core::Vector, - ) -> Option> { + translation: iced_core::Vector, + ) -> Option> { assert_eq!(self.element.thread_id, thread::current().id()); Rc::get_mut(&mut self.element.data).and_then(|e| { e.get_mut() @@ -203,9 +203,9 @@ impl Widget for RcElementWrapper { fn drag_destinations( &self, state: &tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: iced_core::Layout<'_>, renderer: &crate::Renderer, - dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.element.with_data_mut(|e| { e.as_widget_mut() From e5955b568de23b653963cfc8a0ed444633e1795b Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 22:12:36 -0400 Subject: [PATCH 147/168] ci: Updated pages.yml workflow Use nightly channel to enable docs generating feature badges, plus enabled more features in the docs build, and building the cctk docs also --- .github/workflows/pages.yml | 33 ++++++++++++++++++--------------- src/lib.rs | 1 + 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e48570ba..419c99d0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -7,21 +7,24 @@ on: jobs: pages: - runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - submodules: recursive - - name: System dependencies - run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - - name: Build documentation - run: cargo doc --no-deps --verbose --features tokio,winit - - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc - force_orphan: true + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2025-07-31 + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev + - name: Build documentation + run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2025-07-31 doc --no-deps -p cosmic-client-toolkit -p libcosmic --verbose --features tokio,winit,wayland,process,desktop,single-instance + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc + force_orphan: true diff --git a/src/lib.rs b/src/lib.rs index e04f1609..f3873443 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] /// Recommended default imports. pub mod prelude { From 12d2233c6b5f0315e3feb99705d9046f38729a94 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 22:25:25 -0400 Subject: [PATCH 148/168] fix(ci): Added an inline doc to cctk reexport --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index f3873443..02623799 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] pub mod process; +#[doc(inline)] #[cfg(all(feature = "wayland", target_os = "linux"))] pub use cctk; From 6df3f76a33f55de94670e16d1c7e57a1de2fe7f3 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 22:50:13 -0400 Subject: [PATCH 149/168] ci: Added a few more enabled dependency docs --- .github/workflows/pages.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 419c99d0..a15b99b5 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -21,7 +21,15 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation - run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2025-07-31 doc --no-deps -p cosmic-client-toolkit -p libcosmic --verbose --features tokio,winit,wayland,process,desktop,single-instance + run: RUSTDOCFLAGS="--cfg docsrs" \ + cargo +nightly-2025-07-31 doc --no-deps \ + -p cosmic-client-toolkit \ + -p cosmic-protocols \ + -p smithay-client-toolkit \ + -p wayland-protocols \ + -p wayland-client \ + -p libcosmic \ + --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - name: Deploy documentation uses: peaceiris/actions-gh-pages@v3 with: From 77b37f22466ddb581407e0f683c733cf8b7e6891 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 23:01:10 -0400 Subject: [PATCH 150/168] fix(ci) removed the smithay and wayland protocol docs builds --- .github/workflows/pages.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a15b99b5..34e4d0cf 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,9 +25,6 @@ jobs: cargo +nightly-2025-07-31 doc --no-deps \ -p cosmic-client-toolkit \ -p cosmic-protocols \ - -p smithay-client-toolkit \ - -p wayland-protocols \ - -p wayland-client \ -p libcosmic \ --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - name: Deploy documentation From c7093beca323e555f169fe65ac1118e925bd4e75 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Wed, 8 Apr 2026 01:22:23 -0400 Subject: [PATCH 151/168] fix(ci): cargo now running properly --- .github/workflows/pages.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 34e4d0cf..3e3a042e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -21,7 +21,8 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation - run: RUSTDOCFLAGS="--cfg docsrs" \ + run: | + RUSTDOCFLAGS="--cfg docsrs" \ cargo +nightly-2025-07-31 doc --no-deps \ -p cosmic-client-toolkit \ -p cosmic-protocols \ From 47ab72be502378d18b931f0e10e8ba94619dd607 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:38:18 -0400 Subject: [PATCH 152/168] fix!(progress_bar): remove unused generic Message type --- src/widget/progress_bar/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index ea069ffc..4e277b0a 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -3,21 +3,21 @@ pub mod linear; pub mod style; /// A spinner / throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_circular() -> circular::Circular { +pub fn indeterminate_circular() -> circular::Circular { circular::Circular::new() } /// A linear throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_linear() -> linear::Linear { +pub fn indeterminate_linear() -> linear::Linear { linear::Linear::new() } /// A circular progress spinner widget that can be used to indicate the progress of some operation. -pub fn determinate_circular(progress: f32) -> circular::Circular { +pub fn determinate_circular(progress: f32) -> circular::Circular { circular::Circular::new().progress(progress) } /// A linear progress bar widget that can be used to indicate the progress of some operation. -pub fn determinate_linear(progress: f32) -> linear::Linear { +pub fn determinate_linear(progress: f32) -> linear::Linear { linear::Linear::new().progress(progress) } From a44cff8011d81209e18de86f24da248c88b5a28d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Apr 2026 09:58:20 -0400 Subject: [PATCH 153/168] fix(text_input): always clip input text with the text bounds this issue seems unique to tiny-skia --- examples/application/Cargo.toml | 1 - src/widget/text_input/input.rs | 14 ++++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index bc037ec0..c494238f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -18,7 +18,6 @@ features = [ "tokio", "xdg-portal", "a11y", - "wgpu", "single-instance", "surface-message", "multi-window", diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 806ceda0..4336c757 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -2740,14 +2740,14 @@ pub fn draw<'a, Message>( effective_alignment(state.value.raw()), ); - if !cursors.is_empty() { + if cursors.is_empty() { + renderer.with_translation(Vector::ZERO, |_| {}); + } else { renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { for (quad, color) in &cursors { renderer.fill_quad(*quad, *color); } }); - } else { - renderer.with_translation(Vector::ZERO, |_| {}); } let bounds = Rectangle { @@ -2785,11 +2785,9 @@ pub fn draw<'a, Message>( ); }; - if is_selecting { - renderer.with_layer(bounds, render); - } else { - render(renderer); - } + // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. + // Otherwise overflowing text escapes the bounds of the input. + renderer.with_layer(text_bounds, render); let trailing_icon_tree = children.get(child_index); From 6caccaba337ed9bab21c5fe3c2aa7392e322e89c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 8 Apr 2026 16:13:31 -0600 Subject: [PATCH 154/168] fix: icon color when window is maximized --- src/theme/mod.rs | 2 +- src/theme/style/iced.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b7e85237..093bac05 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -307,7 +307,7 @@ impl DefaultStyle for Theme { fn default_style(&self) -> Appearance { let cosmic = self.cosmic(); Appearance { - icon_color: cosmic.bg_color().into(), + icon_color: cosmic.on_bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 4633477d..aa6f4b33 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -43,7 +43,7 @@ pub mod application { iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), - icon_color: cosmic.bg_color().into(), + icon_color: cosmic.on_bg_color().into(), } } } From e287a789c1f33459d4a7ac737c2e7d4004e7e0e4 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 10 Apr 2026 20:53:43 -0600 Subject: [PATCH 155/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7fd263d9..fc6b4634 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 +Subproject commit fc6b46342b365ca4f120a830b66204c2517945c8 From 0e72508dcca7161376e86167242b24e0469e53ee Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 12 Apr 2026 18:50:19 +0200 Subject: [PATCH 156/168] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amadɣas Co-authored-by: Asier Saratsua Garmendia Co-authored-by: ButterflyOfFire Co-authored-by: Ettore Atalan Co-authored-by: Geeson Wan Co-authored-by: Hosted Weblate Co-authored-by: 麋麓 BigELK176 Co-authored-by: 김유빈 Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kab/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hant/ Translation: Pop OS/libcosmic --- i18n/de/libcosmic.ftl | 2 +- i18n/eu/libcosmic.ftl | 0 i18n/kab/libcosmic.ftl | 33 +++++++++++++++++++++++++++++++++ i18n/ko/libcosmic.ftl | 21 ++++++++++++++------- i18n/zh-Hant/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 i18n/eu/libcosmic.ftl diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 238000f5..2d3704a6 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -6,7 +6,7 @@ links = Links developers = Entwickler(innen) designers = Designer(innen) artists = Künstler(innen) -translators = Übersetzer*innen +translators = Übersetzer(innen) documenters = Dokumentierer(innen) # Calendar january = Januar { $year } diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl index e69de29b..6eac2bc7 100644 --- a/i18n/kab/libcosmic.ftl +++ b/i18n/kab/libcosmic.ftl @@ -0,0 +1,33 @@ +close = Mdel +license = Turagt +links = Iseɣwan +developers = Ineflayen +artists = Inaẓuren +translators = Imsuqlen +january = Yennayer { $year } +february = Fuṛar { $year } +march = Meɣres { $year } +april = Yebrir { $year } +may = Mayyu { $year } +june = Yunyu { $year } +july = Yulyu { $year } +august = Ɣuct { $year } +september = Ctembeṛ { $year } +october = Tubeṛ { $year } +november = Wambeṛ { $year } +december = Dujembeṛ { $year } +documenters = Imeskaren +monday = Arim +mon = Ari +tuesday = Aram +tue = Ara +wednesday = Ahad +wed = Aha +thursday = Amhad +thu = Amh +friday = Sem +fri = Sm +saturday = Sed +sat = Sd +sunday = Acer +sun = Ace diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index 8d499756..6cc0adbc 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -2,26 +2,33 @@ february = { $year }년 2월 close = 닫기 documenters = 문서 작성자 november = { $year }년 11월 -friday = 금 -tuesday = 화 +friday = 금요일 +tuesday = 화요일 may = { $year }년 5월 -wednesday = 수 +wednesday = 수요일 april = { $year }년 4월 -monday = 월 +monday = 월요일 translators = 번역가 artists = 아티스트 license = 라이선스 december = { $year }년 12월 -sunday = 일 +sunday = 일요일 links = 링크 march = { $year }년 3월 june = { $year }년 6월 -saturday = 토 +saturday = 토요일 august = { $year }년 8월 developers = 개발자 july = { $year }년 7월 -thursday = 목 +thursday = 목요일 september = { $year }년 9월 designers = 디자이너 october = { $year }년 10월 january = { $year }년 1월 +mon = 월 +tue = 화 +wed = 수 +thu = 목 +fri = 금 +sat = 토 +sun = 일 diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl index e69de29b..8c9b201c 100644 --- a/i18n/zh-Hant/libcosmic.ftl +++ b/i18n/zh-Hant/libcosmic.ftl @@ -0,0 +1,34 @@ +close = 關閉 +developers = 開發人員 +designers = 設計人員 +artists = 美編設計 +translators = 翻譯人員 +documenters = 文件編輯人員 +january = { $year } 年 1 月 +monday = 星期一 +tuesday = 星期二 +wednesday = 星期三 +thursday = 星期四 +friday = 星期五 +saturday = 星期六 +sunday = 星期日 +mon = 週一 +tue = 週二 +wed = 週三 +thu = 週四 +fri = 週五 +sat = 週六 +sun = 週日 +license = 授權 +links = 連結 +february = { $year } 年 2 月 +march = { $year } 年 3 月 +april = { $year } 年 4 月 +may = { $year } 年 5 月 +june = { $year } 年 6 月 +july = { $year } 年 7 月 +august = { $year } 年 8 月 +september = { $year } 年 9 月 +october = { $year } 年 10 月 +november = { $year } 年 11 月 +december = { $year } 年 12 月 From 52116d2f36972c422a0953a4699e32d5eb30cdac Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 13 Apr 2026 14:07:31 -0600 Subject: [PATCH 157/168] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index fc6b4634..78caabba 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fc6b46342b365ca4f120a830b66204c2517945c8 +Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece From 46d9f0c3442189b446ffeff452c314fa6592da7e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 14 Apr 2026 11:53:33 -0700 Subject: [PATCH 158/168] widget/icon: Bundle icons on macOS, not just Windows --- Cargo.toml | 4 ++-- build.rs | 4 +++- src/widget/icon/bundle.rs | 6 +++--- src/widget/icon/named.rs | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83fe90f0..e090ad21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,12 +170,12 @@ cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } zbus = { version = "5.14.0", default-features = false } -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } -[target.'cfg(not(unix))'.dependencies] +[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies] # Used to embed bundled icons for non-unix platforms. phf = { version = "0.13.1", features = ["macros"] } diff --git a/build.rs b/build.rs index c69feaf5..4ce0aa9e 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,9 @@ use std::env; fn main() { println!("cargo::rerun-if-changed=build.rs"); - if env::var_os("CARGO_CFG_UNIX").is_none() { + if env::var_os("CARGO_CFG_UNIX").is_none() + || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") + { generate_bundled_icons(); } } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index 9d0877d0..bb6ce244 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -4,12 +4,12 @@ //! Embedded icons for platforms which do not support icon themes yet. /// Icon bundling is not enabled on unix platforms. -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] pub fn get(icon_name: &str) -> Option { None } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "macos"))] /// Get a bundled icon on non-unix platforms. pub fn get(icon_name: &str) -> Option { ICONS @@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option { .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "macos"))] include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 8405e080..dfd66cf5 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -52,7 +52,7 @@ impl Named { } } - #[cfg(not(windows))] + #[cfg(all(unix, not(target_os = "macos")))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; @@ -107,7 +107,7 @@ impl Named { result } - #[cfg(windows)] + #[cfg(any(not(unix), target_os = "macos"))] #[must_use] pub fn path(self) -> Option { //TODO: implement icon lookup for Windows From 3d8d8915be516229bd215403e0a800ea80f618ae Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 14 Apr 2026 23:14:41 -0600 Subject: [PATCH 159/168] chore: enable ico and xpm image support for desktop feature --- Cargo.toml | 6 ++++++ src/app/mod.rs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e090ad21..d73da2dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ desktop = [ "process", "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", + "dep:image-extras", "dep:mime", "dep:shlex", "tokio?/io-util", @@ -141,9 +142,14 @@ css-color = "0.2.8" derive_setters = "0.1.9" futures = "0.3" image = { version = "0.25.10", default-features = false, features = [ + "ico", "jpeg", "png", ] } +image-extras = { version = "0.1.0", default-features = false, features = [ + "xpm", + "xbm", +], optional = true } libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } diff --git a/src/app/mod.rs b/src/app/mod.rs index 5c0e95e4..42fa4b1b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -128,6 +128,9 @@ impl BootFn, crate::Action(settings: Settings, flags: App::Flags) -> iced::Result { + #[cfg(feature = "desktop")] + image_extras::register(); + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); From 0fc4638af38d8edecf4b0bdc4e17e8e2bd2a2c22 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 15 Apr 2026 14:45:20 -0600 Subject: [PATCH 160/168] fix: register image_extras in run_single_instance too --- src/app/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 42fa4b1b..f78beac7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -197,6 +197,9 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { + #[cfg(feature = "desktop")] + image_extras::register(); + use std::collections::HashMap; let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); From 9cac422c245777e492094177b21b8a8be4ab7bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:03:47 +0200 Subject: [PATCH 161/168] fix(toggler): animate external changes --- src/widget/toggler.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 05371a17..b95b596e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -161,7 +161,10 @@ impl<'a, Message> Widget for Toggler<'a, } fn state(&self) -> tree::State { - tree::State::new(State::default()) + tree::State::new(State { + prev_toggled: self.is_toggled, + ..State::default() + }) } fn id(&self) -> Option { @@ -238,6 +241,14 @@ impl<'a, Message> Widget for Toggler<'a, return; }; let state = tree.state.downcast_mut::(); + + // animate external changes + if state.prev_toggled != self.is_toggled { + state.anim.changed(self.duration); + shell.request_redraw(); + state.prev_toggled = self.is_toggled; + } + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -246,6 +257,7 @@ impl<'a, Message> Widget for Toggler<'a, if mouse_over { shell.publish((on_toggle)(!self.is_toggled)); state.anim.changed(self.duration); + state.prev_toggled = !self.is_toggled; shell.capture_event(); } } @@ -430,4 +442,5 @@ pub fn next_to_each_other( pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, + prev_toggled: bool, } From 9b465a8b5c4d3bb75389bba49d6ee1cec8c26d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:34:25 +0200 Subject: [PATCH 162/168] feat(list_column): button list items --- src/theme/style/button.rs | 30 ++++-- src/widget/list/column.rs | 128 ---------------------- src/widget/list/list_column.rs | 188 +++++++++++++++++++++++++++++++++ src/widget/list/mod.rs | 4 +- src/widget/settings/item.rs | 94 +++++++++++++---- src/widget/settings/section.rs | 17 +-- 6 files changed, 298 insertions(+), 163 deletions(-) delete mode 100644 src/widget/list/column.rs create mode 100644 src/widget/list/list_column.rs diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 0575ce67..bb52d9a6 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -27,7 +27,7 @@ pub enum Button { IconVertical, Image, Link, - ListItem, + ListItem([f32; 4]), MenuFolder, MenuItem, MenuRoot, @@ -148,8 +148,8 @@ pub fn appearance( appearance.text_color = Some(component.on.into()); corner_radii = &cosmic.corner_radii.radius_s; } - Button::ListItem => { - corner_radii = &[0.0; 4]; + Button::ListItem(radii) => { + corner_radii = radii; let (background, text, icon) = color(&cosmic.background.component); if selected { @@ -197,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - appearance(self, focused, selected, false, style, move |component| { + let mut s = appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,7 +209,15 @@ impl Catalog for crate::Theme { }; (component.base.into(), text_color, text_color) - }) + }); + + if let Button::ListItem(_) = style { + if !selected { + s.background = None; + } + } + + s } fn disabled(&self, style: &Self::Class) -> Style { @@ -237,7 +245,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - appearance( + let mut s = appearance( self, focused || matches!(style, Button::Image), selected, @@ -256,7 +264,15 @@ impl Catalog for crate::Theme { (component.hover.into(), text_color, text_color) }, - ) + ); + + if let Button::ListItem(_) = style { + if !selected { + s.background = None; + } + } + + s } fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs deleted file mode 100644 index 945b9140..00000000 --- a/src/widget/list/column.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced_core::Padding; -use iced_widget::container::Catalog; - -use crate::{ - Apply, Element, theme, - widget::{container, divider, space::vertical}, -}; - -#[inline] -pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { - ListColumn::default() -} - -#[must_use] -pub struct ListColumn<'a, Message> { - spacing: u16, - padding: Padding, - list_item_padding: Padding, - divider_padding: u16, - style: theme::Container<'a>, - children: Vec>, -} - -impl Default for ListColumn<'_, Message> { - fn default() -> Self { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = theme::spacing(); - - Self { - spacing: 0, - padding: Padding::from(0), - divider_padding: 16, - list_item_padding: [space_xxs, space_m].into(), - style: theme::Container::List, - children: Vec::with_capacity(4), - } - } -} - -impl<'a, Message: 'static> ListColumn<'a, Message> { - #[inline] - pub fn new() -> Self { - Self::default() - } - - #[allow(clippy::should_implement_trait)] - pub fn add(self, item: impl Into>) -> Self { - #[inline(never)] - fn inner<'a, Message: 'static>( - mut this: ListColumn<'a, Message>, - item: Element<'a, Message>, - ) -> ListColumn<'a, Message> { - if !this.children.is_empty() { - this.children.push( - container(divider::horizontal::default()) - .padding([0, this.divider_padding]) - .into(), - ); - } - - // Ensure a minimum height of 32. - let list_item = crate::widget::row![ - container(item).align_y(iced::Alignment::Center), - vertical().height(iced::Length::Fixed(32.)) - ] - .padding(this.list_item_padding) - .align_y(iced::Alignment::Center); - - this.children.push(list_item.into()); - this - } - - inner(self, item.into()) - } - - #[inline] - pub fn spacing(mut self, spacing: u16) -> Self { - self.spacing = spacing; - self - } - - /// Sets the style variant of this [`Circular`]. - #[inline] - pub fn style(mut self, style: ::Class<'a>) -> Self { - self.style = style; - self - } - - #[inline] - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into(); - self - } - - #[inline] - pub fn divider_padding(mut self, padding: u16) -> Self { - self.divider_padding = padding; - self - } - - pub fn list_item_padding(mut self, padding: impl Into) -> Self { - self.list_item_padding = padding.into(); - self - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - crate::widget::column::with_children(self.children) - .spacing(self.spacing) - .padding(self.padding) - .width(iced::Length::Fill) - .apply(container) - .padding([self.spacing, 0]) - .class(self.style) - .width(iced::Length::Fill) - .into() - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(column: ListColumn<'a, Message>) -> Self { - column.into_element() - } -} diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs new file mode 100644 index 00000000..89a87063 --- /dev/null +++ b/src/widget/list/list_column.rs @@ -0,0 +1,188 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::widget::container::Catalog; +use crate::widget::{button, column, container, divider, row, space::vertical}; +use crate::{Apply, Element, theme}; +use iced::{Length, Padding}; + +/// A button list item for use in a [`ListColumn`]. +pub struct ListButton<'a, Message> { + content: Element<'a, Message>, + on_press: Option, + selected: bool, +} + +/// Creates a [`ListButton`] with the given content. +pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { + ListButton { + content: content.into(), + on_press: None, + selected: false, + } +} + +impl<'a, Message: 'static> ListButton<'a, Message> { + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + pub fn on_press_maybe(mut self, on_press: Option) -> Self { + self.on_press = on_press; + self + } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + +pub enum ListItem<'a, Message> { + Element(Element<'a, Message>), + Button(ListButton<'a, Message>), +} + +/// A trait for types that can be added to a [`ListColumn`]. +pub trait IntoListItem<'a, Message> { + fn into_list_item(self) -> ListItem<'a, Message>; +} + +impl<'a, Message, T> IntoListItem<'a, Message> for T +where + T: Into>, +{ + fn into_list_item(self) -> ListItem<'a, Message> { + ListItem::Element(self.into()) + } +} + +impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { + fn into_list_item(self) -> ListItem<'a, Message> { + ListItem::Button(self) + } +} + +#[must_use] +pub struct ListColumn<'a, Message> { + list_item_padding: Padding, + style: theme::Container<'a>, + children: Vec>, +} + +#[inline] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> { + let cosmic_theme::Spacing { + space_xxs, space_m, .. + } = theme::spacing(); + + ListColumn { + list_item_padding: [space_xxs, space_m].into(), + style: theme::Container::List, + children: Vec::with_capacity(capacity), + } +} + +impl Default for ListColumn<'_, Message> { + fn default() -> Self { + with_capacity(4) + } +} + +impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Adds an element to the list column. + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { + self.children.push(item.into_list_item()); + self + } + + /// Sets the style variant of this [`ListColumn`]. + #[inline] + pub fn style(mut self, style: ::Class<'a>) -> Self { + self.style = style; + self + } + + pub fn list_item_padding(mut self, padding: impl Into) -> Self { + self.list_item_padding = padding.into(); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + let padding = self.list_item_padding; + let count = self.children.len(); + let last_index = count.saturating_sub(1); + let radius_s = theme::active().cosmic().radius_s(); + + // Ensure minimum height of 32 + let content_row = |content| { + row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) + }; + + self.children + .into_iter() + .enumerate() + .fold( + column::with_capacity((2 * count).saturating_sub(1)), + |mut col, (i, item)| { + if i > 0 { + col = col.push(divider::horizontal::default()); + } + + match item { + ListItem::Element(content) => { + col.push(content_row(content).padding(padding).width(Length::Fill)) + } + ListItem::Button(ListButton { + content, + on_press, + selected, + }) => col.push( + content_row(content) + .apply(button::custom) + .padding(padding) + .width(Length::Fill) + .on_press_maybe(on_press) + .selected(selected) + .class(theme::Button::ListItem(get_radius( + radius_s, + i == 0, + i == last_index, + ))), + ), + } + }, + ) + .width(Length::Fill) + .apply(container) + .class(self.style) + .into() + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} + +fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] { + match (first, last) { + (true, true) => radius, + (true, false) => [radius[0], radius[1], 0.0, 0.0], + (false, true) => [0.0, 0.0, radius[2], radius[3]], + (false, false) => [0.0, 0.0, 0.0, 0.0], + } +} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index c6e2051c..71eda086 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,6 +1,6 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -pub mod column; +pub mod list_column; -pub use self::column::{ListColumn, list_column}; +pub use self::list_column::{ListButton, ListColumn, button, list_column}; diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 349d93d8..a4092093 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use crate::{ Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, row, text}, + widget::{FlexRow, Row, column, container, flex_row, list, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; @@ -114,39 +114,95 @@ impl<'a, Message: 'static> Item<'a, Message> { flex_item_row(self.control_(widget.into())) } - #[inline(never)] - fn control_(self, widget: Element<'a, Message>) -> Vec> { - let mut contents = Vec::with_capacity(4); - - if let Some(icon) = self.icon { - contents.push(icon); - } - + fn label(self) -> Element<'a, Message> { if let Some(description) = self.description { - let column = column::with_capacity(2) + column::with_capacity(2) .spacing(2) .push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill); - - contents.push(column.into()); + .width(Length::Fill) + .into() } else { - contents.push(text(self.title).width(Length::Fill).into()); + text(self.title).width(Length::Fill).into() } + } + #[inline(never)] + fn control_(mut self, widget: Element<'a, Message>) -> Vec> { + let mut contents = Vec::with_capacity(3); + if let Some(icon) = self.icon.take() { + contents.push(icon); + } + contents.push(self.label()); contents.push(widget); contents } + fn control_start(self, widget: impl Into>) -> Row<'a, Message, Theme> { + item_row(vec![widget.into(), self.label()]) + } + pub fn toggler( self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> Row<'a, Message, Theme> { - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle(message), + ) -> list::ListButton<'a, Message> { + let on_press = message(!is_checked); + list::button( + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ), ) + .on_press(on_press) + } + + pub fn toggler_maybe( + self, + is_checked: bool, + message: Option Message + 'static>, + ) -> list::ListButton<'a, Message> { + let on_press = message.as_ref().map(|f| f(!is_checked)); + list::button( + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle_maybe(message), + ), + ) + .on_press_maybe(on_press) + } + + pub fn checkbox( + self, + is_checked: bool, + message: impl Fn(bool) -> Message + 'static, + ) -> list::ListButton<'a, Message> { + let on_press = message(!is_checked); + list::button( + self.control_start( + crate::widget::checkbox(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ), + ) + .on_press(on_press) + } + + pub fn checkbox_maybe( + self, + is_checked: bool, + message: Option Message + 'static>, + ) -> list::ListButton<'a, Message> { + let on_press = message.as_ref().map(|f| f(!is_checked)); + list::button( + self.control_start( + crate::widget::checkbox(is_checked) + .width(Length::Shrink) + .on_toggle_maybe(message), + ), + ) + .on_press_maybe(on_press) } } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index ab95b5ad..ee07c76d 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -2,16 +2,19 @@ // SPDX-License-Identifier: MPL-2.0 use crate::Element; +use crate::widget::list_column::IntoListItem; use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: 'static>() -> Section<'a, Message> { +pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } /// A section with a pre-defined list column. -pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { +pub fn with_column( + children: ListColumn<'_, Message>, +) -> Section<'_, Message> { Section { header: None, children, @@ -24,9 +27,9 @@ pub struct Section<'a, Message> { children: ListColumn<'a, Message>, } -impl<'a, Message: 'static> Section<'a, Message> { +impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Define an optional title for the section. - pub fn title(mut self, title: impl Into>) -> Self { + pub fn title(self, title: impl Into>) -> Self { self.header(text::heading(title.into())) } @@ -38,8 +41,8 @@ impl<'a, Message: 'static> Section<'a, Message> { /// Add a child element to the section's list column. #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl Into>) -> Self { - self.children = self.children.add(item.into()); + pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { + self.children = self.children.add(item); self } @@ -61,7 +64,7 @@ impl<'a, Message: 'static> Section<'a, Message> { } } -impl<'a, Message: 'static> From> for Element<'a, Message> { +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) From 917af9fda204d027ad55380521041b6691f17895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:59:37 +0200 Subject: [PATCH 163/168] feat(radio): internal method for radio without label Also adds the related settings item builder. --- src/widget/radio.rs | 165 ++++++++++++++++++++++-------------- src/widget/settings/item.rs | 16 +++- 2 files changed, 116 insertions(+), 65 deletions(-) diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 338c0a4e..c3f115c0 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,5 +1,5 @@ //! Create choices using radio buttons. -use crate::Theme; +use crate::{Theme, theme}; use iced::border; use iced_core::event::{self, Event}; use iced_core::layout; @@ -92,7 +92,7 @@ where { is_selected: bool, on_click: Message, - label: Element<'a, Message, Theme, Renderer>, + label: Option>, width: Length, size: f32, spacing: f32, @@ -106,9 +106,6 @@ where /// The default size of a [`Radio`] button. pub const DEFAULT_SIZE: f32 = 16.0; - /// The default spacing of a [`Radio`] button. - pub const DEFAULT_SPACING: f32 = 8.0; - /// Creates a new [`Radio`] button. /// /// It expects: @@ -126,10 +123,29 @@ where Radio { is_selected: Some(value) == selected, on_click: f(value), - label: label.into(), + label: Some(label.into()), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, + spacing: theme::spacing().space_xs as f32, + } + } + + /// Creates a new [`Radio`] button without a label. + /// + /// This is intended for internal use with the settings item builder, + /// where the label comes from the settings item title instead. + pub(crate) fn new_no_label(value: V, selected: Option, f: F) -> Self + where + V: Eq + Copy, + F: FnOnce(V) -> Message, + { + Radio { + is_selected: Some(value) == selected, + on_click: f(value), + label: None, + width: Length::Shrink, + size: Self::DEFAULT_SIZE, + spacing: theme::spacing().space_xs as f32, } } @@ -161,11 +177,17 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - vec![Tree::new(&self.label)] + if let Some(label) = &self.label { + vec![Tree::new(label)] + } else { + vec![] + } } fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.label)); + if let Some(label) = &mut self.label { + tree.diff_children(std::slice::from_mut(label)); + } } fn size(&self) -> Size { Size { @@ -180,16 +202,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::next_to_each_other( - &limits.width(self.width), - self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), - |limits| { - self.label - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) + if let Some(label) = &mut self.label { + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + label + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + }, + ) + } else { + layout::Node::new(Size::new(self.size, self.size)) + } } fn operate( @@ -199,12 +225,14 @@ where renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); + if let Some(label) = &mut self.label { + label.as_widget_mut().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); + } } fn update( @@ -218,24 +246,25 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.label.as_widget_mut().update( - &mut tree.children[0], - event, - layout.children().nth(1).unwrap(), - cursor, - renderer, - clipboard, - shell, - viewport, - ); + if let Some(label) = &mut self.label { + label.as_widget_mut().update( + &mut tree.children[0], + event, + layout.children().nth(1).unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } if !shell.is_event_captured() { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - shell.capture_event(); return; } @@ -253,13 +282,17 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let interaction = self.label.as_widget().mouse_interaction( - &tree.children[0], - layout.children().nth(1).unwrap(), - cursor, - viewport, - renderer, - ); + let interaction = if let Some(label) = &self.label { + label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ) + } else { + mouse::Interaction::default() + }; if interaction == mouse::Interaction::default() { if cursor.is_over(layout.bounds()) { @@ -284,8 +317,6 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); - let mut children = layout.children(); - let custom_style = if is_mouse_over { theme.style( &(), @@ -302,16 +333,21 @@ where ) }; - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); + let (dot_bounds, label_layout) = if self.label.is_some() { + let mut children = layout.children(); + let dot_bounds = children.next().unwrap().bounds(); + (dot_bounds, children.next()) + } else { + (layout.bounds(), None) + }; - let size = bounds.width; + { + let size = dot_bounds.width; let dot_size = 6.0; renderer.fill_quad( renderer::Quad { - bounds, + bounds: dot_bounds, border: Border { radius: (size / 2.0).into(), width: custom_style.border_width, @@ -326,8 +362,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + (size - dot_size) / 2.0, - y: bounds.y + (size - dot_size) / 2.0, + x: dot_bounds.x + (size - dot_size) / 2.0, + y: dot_bounds.y + (size - dot_size) / 2.0, width: dot_size, height: dot_size, }, @@ -339,9 +375,8 @@ where } } - { - let label_layout = children.next().unwrap(); - self.label.as_widget().draw( + if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { + label.as_widget().draw( &tree.children[0], renderer, theme, @@ -361,7 +396,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - self.label.as_widget_mut().overlay( + self.label.as_mut()?.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -377,12 +412,14 @@ where renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - self.label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); + if let Some(label) = &self.label { + label.as_widget().drag_destinations( + &state.children[0], + layout.children().nth(1).unwrap(), + renderer, + dnd_rectangles, + ); + } } } diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index a4092093..11821335 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -103,7 +103,7 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: 'static> Item<'a, Message> { +impl<'a, Message: Clone + 'static> Item<'a, Message> { /// Assigns a control to the item. pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { item_row(self.control_(widget.into())) @@ -205,4 +205,18 @@ impl<'a, Message: 'static> Item<'a, Message> { ) .on_press_maybe(on_press) } + + pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> + where + V: Eq + Copy + 'static, + F: Fn(V) -> Message + 'static, + { + let on_press = f(value); + list::button( + self.control_start(crate::widget::radio::Radio::new_no_label( + value, selected, f, + )), + ) + .on_press(on_press) + } } From 3f9e93067b31d9ba81a4e3a28653b3380c61c352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:08:42 +0200 Subject: [PATCH 164/168] fix(item builder): remove unnecessary lifetime bound for radio --- src/widget/settings/item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 11821335..5abb464c 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -208,8 +208,8 @@ impl<'a, Message: Clone + 'static> Item<'a, Message> { pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> where - V: Eq + Copy + 'static, - F: Fn(V) -> Message + 'static, + V: Eq + Copy, + F: Fn(V) -> Message, { let on_press = f(value); list::button( From c162a1f24a2b7fdf29286dfa807c4a1b4813ab7c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 9 Apr 2026 18:32:14 -0600 Subject: [PATCH 165/168] fix(animated-image): update frames and fix compilation errors --- src/widget/frames.rs | 63 +++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/src/widget/frames.rs b/src/widget/frames.rs index 056a55ba..a542cec6 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; use iced_core::{ - Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, - event, layout, renderer, window, + Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, + Widget, event, layout, renderer, window, }; -use iced_widget::image::{self, Handle}; +use iced_widget::image::{self, FilterMethod, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::png::PngDecoder; @@ -146,7 +146,7 @@ impl Frames { match image_type { ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), - ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), + ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), } } @@ -168,10 +168,10 @@ impl Frames { let first = frames.first().cloned().unwrap(); let total_bytes = frames .iter() - .map(|f| match f.handle.data() { - iced_core::image::Handle::Path(..) => 0, - iced_core::image::Handle::Bytes(_, b) => b.len(), - iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), + .map(|f| match &f.handle { + Handle::Path(..) => 0, + Handle::Bytes(_, b) => b.len(), + Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -324,7 +324,11 @@ where &self.frames.first.handle, self.width, self.height, + None, self.content_fit, + Rotation::default(), + false, + [0.0; 4], ) } @@ -371,37 +375,18 @@ where ) { let state = tree.state.downcast_ref::(); - // Pulled from iced_native::widget::::draw - // - // TODO: export iced_native::widget::image::draw as standalone function - { - let Size { width, height } = renderer.dimensions(&state.current.frame.handle); - let image_size = Size::new(width as f32, height as f32); - - let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); - - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset); - }; - - if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { - renderer.with_layer(bounds, render); - } else { - render(renderer); - } - } + iced_widget::image::draw( + renderer, + layout, + &state.current.frame.handle, + None, + iced_core::border::Radius::default(), + self.content_fit, + FilterMethod::default(), + Rotation::default(), + 1.0, + 1.0, + ); } } From 8d7bcab258ba61dc8184d85b63a0e689aefd085c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:45:33 +0200 Subject: [PATCH 166/168] fix(list_column): add back `divider_padding` Also matches previous behavior of both paddings being applied to subsequent items, rather than globally. --- src/widget/list/list_column.rs | 101 ++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 89a87063..4ef3fc01 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -64,11 +64,19 @@ impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { } } +// Snapshots the padding values at the moment an item is added +struct ListEntry<'a, Message> { + item: ListItem<'a, Message>, + item_padding: Padding, + divider_padding: u16, +} + #[must_use] pub struct ListColumn<'a, Message> { list_item_padding: Padding, + divider_padding: u16, style: theme::Container<'a>, - children: Vec>, + children: Vec>, } #[inline] @@ -83,6 +91,7 @@ pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Me ListColumn { list_item_padding: [space_xxs, space_m].into(), + divider_padding: 0, style: theme::Container::List, children: Vec::with_capacity(capacity), } @@ -100,10 +109,14 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { Self::default() } - /// Adds an element to the list column. + /// Adds a [`ListItem`] to the [`ListColumn`]. #[allow(clippy::should_implement_trait)] pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(item.into_list_item()); + self.children.push(ListEntry { + item: item.into_list_item(), + item_padding: self.list_item_padding, + divider_padding: self.divider_padding, + }); self } @@ -119,53 +132,65 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { self } + #[inline] + pub fn divider_padding(mut self, padding: u16) -> Self { + self.divider_padding = padding; + self + } + #[must_use] pub fn into_element(self) -> Element<'a, Message> { - let padding = self.list_item_padding; let count = self.children.len(); let last_index = count.saturating_sub(1); let radius_s = theme::active().cosmic().radius_s(); + let mut col = column::with_capacity((2 * count).saturating_sub(1)); // Ensure minimum height of 32 let content_row = |content| { row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) }; - self.children - .into_iter() - .enumerate() - .fold( - column::with_capacity((2 * count).saturating_sub(1)), - |mut col, (i, item)| { - if i > 0 { - col = col.push(divider::horizontal::default()); - } + for ( + i, + ListEntry { + item, + item_padding, + divider_padding, + }, + ) in self.children.into_iter().enumerate() + { + if i > 0 { + col = col + .push(container(divider::horizontal::default()).padding([0, divider_padding])); + } - match item { - ListItem::Element(content) => { - col.push(content_row(content).padding(padding).width(Length::Fill)) - } - ListItem::Button(ListButton { - content, - on_press, - selected, - }) => col.push( - content_row(content) - .apply(button::custom) - .padding(padding) - .width(Length::Fill) - .on_press_maybe(on_press) - .selected(selected) - .class(theme::Button::ListItem(get_radius( - radius_s, - i == 0, - i == last_index, - ))), - ), - } - }, - ) - .width(Length::Fill) + col = match item { + ListItem::Element(content) => col.push( + content_row(content) + .padding(item_padding) + .width(Length::Fill), + ), + ListItem::Button(ListButton { + content, + on_press, + selected, + }) => col.push( + content_row(content) + .apply(button::custom) + .padding(item_padding) + .width(Length::Fill) + .on_press_maybe(on_press) + .selected(selected) + .class(theme::Button::ListItem(get_radius( + radius_s, + i == 0, + i == last_index, + ))), + ), + }; + } + + col.width(Length::Fill) .apply(container) .class(self.style) .into() From c423ad1bfc25057922406c687f2ddc75ead5ab67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:19:23 +0200 Subject: [PATCH 167/168] improv(about): use `ListButton` --- src/widget/about.rs | 25 +++++++++++++++---------- src/widget/settings/section.rs | 11 ++++++++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/widget/about.rs b/src/widget/about.rs index 148af02a..9b21e93a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,8 +1,9 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, space}, + widget::{self, list}, }; +use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] #[setters(into, strip_option)] @@ -104,19 +105,23 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { - widget::row::with_capacity(3) - .push(widget::text(name)) - .push(space::horizontal()) + let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { + color: Some(theme.cosmic().accent_text_color().into()), + }); + + let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> { + widget::row::with_capacity(2) + .push(widget::text::body(name).width(Length::Fill)) .push_maybe( - (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), + (!url.is_empty()).then_some( + widget::icon::from_name("link-symbolic") + .icon() + .class(crate::theme::Svg::Custom(svg_accent.clone())), + ), ) .align_y(Alignment::Center) - .apply(widget::button::custom) - .class(crate::theme::Button::Link) + .apply(list::button) .on_press(on_url_press(url)) - .width(Length::Fill) - .into() }; let section = |list: &'a Vec<(String, String)>, title: String| { diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index ee07c76d..3dddb1a1 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -3,7 +3,7 @@ use crate::Element; use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, text}; +use crate::widget::{ListColumn, column, list_column, text}; use std::borrow::Cow; /// A section within a settings view column. @@ -11,6 +11,11 @@ pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } +/// A section with a pre-defined list column of a given capacity. +pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> { + with_column(list_column::with_capacity(capacity)) +} + /// A section with a pre-defined list column. pub fn with_column( children: ListColumn<'_, Message>, @@ -47,7 +52,7 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { } /// Add a child element to the section's list column, if `Some`. - pub fn add_maybe(self, item: Option>>) -> Self { + pub fn add_maybe(self, item: Option>) -> Self { if let Some(item) = item { self.add(item) } else { @@ -58,7 +63,7 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Extends the [`Section`] with the given children. pub fn extend( self, - children: impl IntoIterator>>, + children: impl IntoIterator>, ) -> Self { children.into_iter().fold(self, Self::add) } From 95756b1a576cf6dc9f6135cf1c66e1283bfc487f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:33:57 +0200 Subject: [PATCH 168/168] improv(circular): prevent caps from touching --- examples/application/Cargo.toml | 1 + examples/application/src/main.rs | 3 +- src/widget/progress_bar/circular.rs | 57 +++++++++++++++++------------ 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index c494238f..7a6083e0 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -21,4 +21,5 @@ features = [ "single-instance", "surface-message", "multi-window", + "wgpu", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index bceece6e..f6e571e0 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -200,7 +200,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(5) + widget::column::with_capacity(14) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -223,6 +223,7 @@ impl cosmic::Application for App { .on_clear(Message::Ignore), ) .push(widget::progress_bar::circular::Circular::new().size(50.0)) + .push(widget::progress_bar::circular::Circular::new().size(20.0)) .push( widget::progress_bar::linear::Linear::new() .girth(10.0) diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 7e8177d6..fa8c38fe 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -15,8 +15,6 @@ use std::f32::consts::PI; use std::time::Duration; const MIN_ANGLE: Radians = Radians(PI / 8.0); -const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0); -const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; #[must_use] pub struct Circular @@ -83,6 +81,12 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } + + fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { + let cap_angle = self.bar_height / track_radius; + let gap = MIN_ANGLE.0.max(cap_angle); + (gap - cap_angle, 2.0 * PI - gap * 2.0) + } } impl Default for Circular @@ -122,7 +126,7 @@ impl Default for Animation { } impl Animation { - fn next(&self, additional_rotation: u32, now: Instant) -> Self { + fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { match self { Self::Expanding { rotation, .. } => Self::Contracting { start: now, @@ -133,9 +137,9 @@ impl Animation { Self::Contracting { rotation, .. } => Self::Expanding { start: now, progress: 0.0, - rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( - (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, - )), + rotation: rotation.wrapping_add( + (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, + ), last: now, }, } @@ -157,6 +161,7 @@ impl Animation { &self, cycle_duration: Duration, rotation_duration: Duration, + wrap_angle: f32, now: Instant, ) -> Self { let elapsed = now.duration_since(self.start()); @@ -165,7 +170,7 @@ impl Animation { * (u32::MAX) as f32) as u32; match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), + elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), } } @@ -267,10 +272,13 @@ where return; } if let Event::Window(window::Event::RedrawRequested(now)) = event { - state.animation = - state - .animation - .timed_transition(self.cycle_duration, self.rotation_duration, *now); + let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.rotation_duration, + wrap_angle, + *now, + ); state.cache.clear(); shell.request_redraw(); @@ -380,22 +388,23 @@ where } else { let mut builder = canvas::path::Builder::new(); - let start = Radians(state.animation.rotation() * 2.0 * PI); + let start = state.animation.rotation() * 2.0 * PI; + let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); let (start_angle, end_angle) = match state.animation { Animation::Expanding { progress, .. } => ( start, - start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)), + start + min_angle + wrap_angle * smootherstep(progress), ), Animation::Contracting { progress, .. } => ( - start + WRAP_ANGLE * (smootherstep(progress)), - start + MIN_ANGLE + WRAP_ANGLE, + start + wrap_angle * smootherstep(progress), + start + min_angle + wrap_angle, ), }; builder.arc(canvas::path::Arc { center: frame.center(), radius: track_radius, - start_angle, - end_angle, + start_angle: Radians(start_angle), + end_angle: Radians(end_angle), }); let bar_path = builder.build(); @@ -410,23 +419,23 @@ where let mut builder = canvas::path::Builder::new(); // get center of end of arc for rounded cap - let end_center = frame.center() - + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; + let end_center = + frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; builder.arc(canvas::path::Arc { center: end_center, radius: self.bar_height / 2.0, - start_angle: Radians(end_angle.0), - end_angle: Radians(end_angle.0 + PI), + start_angle: Radians(end_angle), + end_angle: Radians(end_angle + PI), }); // get center of start of arc for rounded cap let start_center = frame.center() - + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; + + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; builder.arc(canvas::path::Arc { center: start_center, radius: self.bar_height / 2.0, - start_angle: Radians(start_angle.0 - PI), - end_angle: Radians(start_angle.0), + start_angle: Radians(start_angle - PI), + end_angle: Radians(start_angle), }); let cap_path = builder.build();