From 92023835963f17c01b370d751fb54a3ad9117d99 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 14:01:42 -0500 Subject: [PATCH 0001/1050] chore: update to 0.12 --- Cargo.toml | 2 - cosmic-config/src/settings_daemon.rs | 0 examples/cosmic/Cargo.toml | 1 + examples/cosmic/src/window.rs | 5 +- examples/cosmic/src/window/demo.rs | 4 +- iced | 2 +- src/app/cosmic.rs | 3 +- src/app/mod.rs | 2 +- src/app/settings.rs | 2 +- src/font.rs | 10 +- src/keyboard_nav.rs | 3 +- src/lib.rs | 1 + src/theme/style/iced.rs | 17 - src/widget/aspect_ratio.rs | 10 +- src/widget/button/widget.rs | 13 +- src/widget/color_picker/mod.rs | 11 +- src/widget/context_drawer/overlay.rs | 12 +- src/widget/context_drawer/widget.rs | 11 +- src/widget/cosmic_container.rs | 13 +- src/widget/dropdown/menu/mod.rs | 42 +- src/widget/dropdown/widget.rs | 132 +++-- src/widget/flex_row/layout.rs | 6 +- src/widget/flex_row/widget.rs | 8 +- src/widget/grid/layout.rs | 16 +- src/widget/grid/widget.rs | 8 +- src/widget/menu/flex.rs | 14 +- src/widget/menu/menu_bar.rs | 4 +- src/widget/menu/menu_inner.rs | 74 ++- src/widget/popover.rs | 18 +- src/widget/rectangle_tracker/mod.rs | 8 +- src/widget/segmented_button/horizontal.rs | 11 +- src/widget/segmented_button/vertical.rs | 11 +- src/widget/segmented_button/widget.rs | 115 +++-- src/widget/text_input/input.rs | 577 +++++++++++----------- 34 files changed, 712 insertions(+), 454 deletions(-) create mode 100644 cosmic-config/src/settings_daemon.rs diff --git a/Cargo.toml b/Cargo.toml index 962c14f1..aeb86e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,5 +143,3 @@ exclude = ["examples/design-demo", "iced"] [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./" } -# [patch."https://github.com/pop-os/cosmic-time"] -# cosmic-time = { path = "../cosmic-time" } diff --git a/cosmic-config/src/settings_daemon.rs b/cosmic-config/src/settings_daemon.rs new file mode 100644 index 00000000..e69de29b diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index a4c3bf3e..fdc939b9 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -17,4 +17,5 @@ log = "0.4.17" [dependencies.cosmic-time] git = "https://github.com/pop-os/cosmic-time" default-features = false +branch = "update-0.12" features = ["libcosmic", "once_cell"] \ No newline at end of file diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 023dadf7..c8e67484 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -12,6 +12,7 @@ use cosmic::{ widget::{self, column, container, horizontal_space, row, text}, window::{self, close, drag, minimize, toggle_maximize}, }, + iced_futures::event::listen_raw, keyboard_nav, prelude::*, theme::{self, Theme}, @@ -358,7 +359,7 @@ impl Application for Window { } fn subscription(&self) -> Subscription { - let window_break = subscription::events_with(|event, _| match event { + let window_break = listen_raw(|event, _| match event { cosmic::iced::Event::Window( _window_id, window::Event::Resized { width, height: _ }, @@ -450,7 +451,7 @@ impl Application for Window { _ => (), }, Message::ToggleWarning => self.toggle_warning(), - Message::FontsLoaded => {} + Message::FontsLoaded => {} // Message::Tick(instant) => self.timeline.borrow_mut().now(instant), Message::Tick(instant) => self.timeline.borrow_mut().now(instant), Message::Tick(instant) => self.timeline.borrow_mut().now(instant), } ret diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 5a484f09..824b243d 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -4,7 +4,8 @@ use apply::Apply; use cosmic::{ cosmic_theme, iced::widget::{checkbox, column, progress_bar, radio, slider, text, text_input}, - iced::{id, Alignment, Length}, + iced::{Alignment, Length}, + iced_core::id, theme::ThemeType, widget::{ button, color_picker::ColorPickerUpdate, cosmic_container::container, dropdown, icon, @@ -498,7 +499,6 @@ impl State { .on_input(Message::InputChanged) .into(), cosmic::widget::search_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) .width(Length::Fixed(100.0)) .on_input(Message::InputChanged) .into(), diff --git a/iced b/iced index b3ede4f9..9af6bbb5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b3ede4f9a72275cfeb29fac80a31546f728783fd +Subproject commit 9af6bbb55c3443a21b835b01f20b2c0032cb50bc diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index f621c792..33042725 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -13,6 +13,7 @@ use iced::event::wayland::{self, WindowEvent}; #[cfg(feature = "wayland")] use iced::event::PlatformSpecific; use iced::window; +use iced_futures::event::listen_raw; #[cfg(not(feature = "wayland"))] use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] @@ -126,7 +127,7 @@ where } fn subscription(&self) -> Subscription { - let window_events = iced::subscription::events_with(|event, _| { + let window_events = listen_raw(|event, _| { match event { iced::Event::Window(id, window::Event::Resized { width, height }) => { return Some(Message::WindowResize(id, width, height)); diff --git a/src/app/mod.rs b/src/app/mod.rs index 328fa9c6..f775aba1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -82,7 +82,7 @@ pub(crate) fn iced_settings( iced.antialiasing = settings.antialiasing; iced.default_font = settings.default_font; - iced.default_text_size = settings.default_text_size; + iced.default_text_size = iced::Pixels(settings.default_text_size); iced.exit_on_close_request = settings.exit_on_close; iced.id = Some(App::APP_ID.to_owned()); diff --git a/src/app/settings.rs b/src/app/settings.rs index 10ed4dd3..21d649f7 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -5,7 +5,7 @@ use crate::{font, Theme}; #[cfg(feature = "wayland")] -use iced::Limits; +use iced_core::layout::Limits; use iced_core::Font; /// Configure a new COSMIC application. diff --git a/src/font.rs b/src/font.rs index 8f1be266..926ef14d 100644 --- a/src/font.rs +++ b/src/font.rs @@ -16,7 +16,7 @@ pub const FONT: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Normal, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Regular.otf"); @@ -25,7 +25,7 @@ pub const FONT_LIGHT: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Light, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_LIGHT_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Light.otf"); @@ -34,7 +34,7 @@ pub const FONT_SEMIBOLD: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Semibold, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_SEMIBOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-SemiBold.otf"); @@ -43,7 +43,7 @@ pub const FONT_BOLD: Font = Font { family: Family::Name("Fira Sans"), weight: iced_core::font::Weight::Bold, stretch: iced_core::font::Stretch::Normal, - monospaced: false, + style: iced_core::font::Style::Normal, }; pub const FONT_BOLD_DATA: &[u8] = include_bytes!("../res/Fira/FiraSans-Bold.otf"); @@ -52,7 +52,7 @@ pub const FONT_MONO_REGULAR: Font = Font { family: Family::Name("Fira Mono"), weight: iced_core::font::Weight::Normal, stretch: iced_core::font::Stretch::Normal, - monospaced: true, + style: iced_core::font::Style::Normal, }; pub const FONT_MONO_REGULAR_DATA: &[u8] = include_bytes!("../res/Fira/FiraMono-Regular.otf"); diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 40294a07..1f3e7ffd 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -12,6 +12,7 @@ use iced_core::{ widget::{operation, Id, Operation}, Rectangle, }; +use iced_futures::event::listen_raw; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Message { @@ -24,7 +25,7 @@ pub enum Message { } pub fn subscription() -> Subscription { - subscription::events_with(|event, status| { + listen_raw(|event, status| { if event::Status::Ignored != status { return None; } diff --git a/src/lib.rs b/src/lib.rs index 6a65e18d..b00ad829 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,5 +66,6 @@ pub use theme::{style, Theme}; pub mod widget; +type Paragraph = ::Paragraph; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index d4868618..27635a5a 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -146,23 +146,6 @@ impl iced_button::StyleSheet for Theme { } } - fn focused(&self, style: &Self::Style) -> iced_button::Appearance { - if let Button::Custom { hover, .. } = style { - return hover(self); - } - - let active = self.active(style); - let component = style.cosmic(self); - iced_button::Appearance { - background: match style { - Button::Link => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.hover.into())), - }, - ..active - } - } - fn disabled(&self, style: &Self::Style) -> iced_button::Appearance { let active = self.active(style); diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 076abdb6..471cfaaf 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -161,12 +161,18 @@ where Widget::height(&self.container) } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { let custom_limits = layout::Limits::new( self.constrain_limits(limits.min()), self.constrain_limits(limits.max()), ); - self.container.layout(renderer, &custom_limits) + self.container + .layout(&mut tree.children[0], renderer, &custom_limits) } fn operate( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 59799399..37cb58f6 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -244,14 +244,23 @@ where self.height } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { layout( renderer, limits, self.width, self.height, self.padding, - |renderer, limits| self.content.as_widget().layout(renderer, limits), + |renderer, limits| { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + }, ) } diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 327b0b7f..f9d116b9 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -515,8 +515,15 @@ where Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { - self.inner.as_widget().layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.inner + .as_widget() + .layout(&mut tree.children[0], renderer, limits) } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 3c84cde9..f2a2bc79 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -20,12 +20,20 @@ impl<'a, 'b, Message> overlay::Overlay for Overlay<'a, where Message: Clone, { - fn layout(&self, renderer: &crate::Renderer, bounds: Size, position: Point) -> layout::Node { + fn layout( + &mut self, + renderer: &crate::Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { let limits = layout::Limits::new(Size::ZERO, bounds) .width(self.width) .height(bounds.height - 8.0 - position.y); - let mut node = self.content.as_widget().layout(renderer, &limits); + let mut node = + self.content + .as_widget() + .layout(&mut self.tree.children[0], renderer, &limits); let node_size = node.size(); node.move_to(Point { diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index c0cd941b..dee04a86 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -122,8 +122,15 @@ impl<'a, Message: Clone> Widget for ContextDrawer<'a, Message self.content.as_widget().height() } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) } fn operate( diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 7d170dfd..4f9c7b78 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -144,6 +144,10 @@ where self.container.diff(tree); } + fn state(&self) -> iced_core::widget::tree::State { + self.container.state() + } + fn width(&self) -> Length { Widget::width(&self.container) } @@ -152,8 +156,13 @@ where Widget::height(&self.container) } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.container.layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.container.layout(tree, renderer, limits) } fn operate( diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 4797adb1..589ff295 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -187,7 +187,12 @@ impl<'a, Message: 'a> Overlay<'a, Message> { } impl<'a, Message> iced_core::Overlay for Overlay<'a, Message> { - fn layout(&self, renderer: &crate::Renderer, bounds: Size, position: Point) -> layout::Node { + fn layout( + &mut self, + renderer: &crate::Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -204,7 +209,7 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M ) .width(self.width); - let mut node = self.container.layout(renderer, &limits); + let mut node = self.container.layout(&mut self.state, renderer, &limits); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) @@ -289,13 +294,18 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + _tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { use std::f32; let limits = limits.width(Length::Fill).height(Length::Shrink); let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let text_line_height = self.text_line_height.to_absolute(Pixels(text_size)); @@ -335,7 +345,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S if let Some(cursor_position) = cursor.position_in(layout.bounds()) { let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) @@ -356,7 +366,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S if let Some(cursor_position) = cursor.position_in(layout.bounds()) { let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) @@ -408,7 +418,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .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(); @@ -484,24 +494,26 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S (appearance.text_color, crate::font::FONT) }; + let bounds = Rectangle { + x: bounds.x + self.padding.left, + y: bounds.center_y(), + width: f32::INFINITY, + ..bounds + }; text::Renderer::fill_text( renderer, Text { content: option.as_ref(), - bounds: Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: f32::INFINITY, - ..bounds - }, - size: text_size, + bounds: bounds.size(), + size: Pixels(text_size), line_height: self.text_line_height, font, - color, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + color, ); } } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index fbcf7dcb..632eaf56 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -6,7 +6,7 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; use iced_core::event::{self, Event}; -use iced_core::text::{self, Text}; +use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget}; @@ -61,6 +61,28 @@ impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { font: None, } } + + fn update_paragraphs(&self, state: &mut tree::State) { + let state = state.downcast_mut::(); + + state + .selections + .resize_with(self.selections.len(), crate::Paragraph::new); + for (i, selection) in self.selections.iter().enumerate() { + state.selections[i].update(Text { + content: selection.as_ref(), + bounds: Size::INFINITY, + // 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(crate::font::FONT), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + } + } } impl<'a, S: AsRef, Message: 'a> Widget for Dropdown<'a, S, Message> { @@ -69,7 +91,31 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo } fn state(&self) -> tree::State { - tree::State::new(State::new()) + let mut tree = tree::State::new(State::new()); + self.update_paragraphs(&mut tree); + tree + } + + fn diff(&mut self, tree: &mut Tree) { + let state = tree.state.downcast_mut::(); + + state + .selections + .resize_with(self.selections.len(), crate::Paragraph::new); + for (i, selection) in self.selections.iter().enumerate() { + state.selections[i].update(Text { + content: selection.as_ref(), + bounds: Size::INFINITY, + // 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(crate::font::FONT), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + } } fn width(&self) -> Length { @@ -80,7 +126,12 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { layout( renderer, limits, @@ -90,9 +141,12 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo self.text_size.unwrap_or(14.0), self.text_line_height, self.font, - self.selected - .and_then(|id| self.selections.get(id)) - .map(AsRef::as_ref), + self.selected.and_then(|id| { + self.selections + .get(id) + .map(AsRef::as_ref) + .zip(tree.state.downcast_mut::().selections.get_mut(id)) + }), ) } @@ -173,6 +227,8 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo self.gap, self.padding, self.text_size.unwrap_or(14.0), + self.text_line_height, + self.font, self.selections, self.selected, &self.on_selected, @@ -196,6 +252,7 @@ pub struct State { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, + selections: Vec, } impl State { @@ -214,6 +271,7 @@ impl State { keyboard_modifiers: keyboard::Modifiers::default(), is_open: false, hovered_option: None, + selections: Vec::new(), } } } @@ -235,7 +293,7 @@ pub fn layout( text_size: f32, text_line_height: text::LineHeight, font: Option, - selection: Option<&str>, + selection: Option<(&str, &mut crate::Paragraph)>, ) -> layout::Node { use std::f32; @@ -243,16 +301,18 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size, - font.unwrap_or_else(|| text::Renderer::default_font(renderer)), - text::Shaping::Advanced, - ); - - width.round() + let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 { + paragraph.update(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(|| text::Renderer::default_font(renderer)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + paragraph.min_width().round() }; selection.map(measure).unwrap_or_default() @@ -358,6 +418,8 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( gap: f32, padding: Padding, text_size: f32, + text_line_height: text::LineHeight, + font: Option, selections: &'a [S], selected_option: Option, on_selected: &'a dyn Fn(usize) -> Message, @@ -378,21 +440,14 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( None, ) .width({ - let measure = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size, - crate::font::FONT, - text::Shaping::Advanced, - ); - - width.round() + let measure = |label: &str, selection_paragraph: &mut crate::Paragraph| -> f32 { + selection_paragraph.min_width().round() }; selections .iter() - .map(|label| measure(label.as_ref())) + .zip(state.selections.iter_mut()) + .map(|(label, selection)| measure(label.as_ref(), selection)) .fold(0.0, |next, current| current.max(next)) + gap + 16.0 @@ -462,26 +517,27 @@ pub fn draw<'a, S>( } if let Some(content) = selected.map(AsRef::as_ref) { - let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer)); - + let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); + let bounds = Rectangle { + x: bounds.x + padding.left, + y: bounds.center_y(), + width: bounds.width - padding.horizontal(), + height: f32::from(text_line_height.to_absolute(Pixels(text_size))), + }; text::Renderer::fill_text( renderer, Text { content, - size: text_size, + size: iced::Pixels(text_size), line_height: text_line_height, font, - color: style.text_color, - bounds: Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.horizontal(), - height: f32::from(text_line_height.to_absolute(Pixels(text_size))), - }, + bounds: bounds.size(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + style.text_color, ); } } diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index b1b75b28..ba4de64e 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -3,6 +3,7 @@ use crate::{Element, Renderer}; use iced_core::layout::{Limits, Node}; +use iced_core::widget::Tree; use iced_core::{Padding, Point, Size}; pub fn resolve( @@ -12,6 +13,7 @@ pub fn resolve( padding: Padding, column_spacing: f32, row_spacing: f32, + tree: &mut [Tree], ) -> Node { let limits = limits.pad(padding); @@ -26,9 +28,9 @@ pub fn resolve( let mut row_buffer = Vec::::with_capacity(8); - for child in items { + for (child, tree) in items.iter().zip(tree.into_iter()) { // Calculate the dimensions of the item. - let child_node = child.as_widget().layout(renderer, &limits); + let child_node = child.as_widget().layout(tree, renderer, &limits); let size = child_node.size(); // Calculate the required additional width to fit the item into the current row. diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 667b44b0..e969bfb8 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -58,7 +58,12 @@ impl<'a, Message: 'static + Clone> Widget for FlexRow<'a, Mes Length::Shrink } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits .max_width(self.max_width) .width(self.width()) @@ -71,6 +76,7 @@ impl<'a, Message: 'static + Clone> Widget for FlexRow<'a, Mes self.padding, f32::from(self.column_spacing), f32::from(self.row_spacing), + &mut tree.children, ) } diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index 79f6efcd..f746e88b 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -4,6 +4,7 @@ use super::widget::Assignment; use crate::{Element, Renderer}; use iced_core::layout::{Limits, Node}; +use iced_core::widget::Tree; use iced_core::{Alignment, Length, Padding, Point, Size}; use taffy::geometry::{Line, Rect}; @@ -24,6 +25,7 @@ pub fn resolve( row_alignment: Alignment, column_spacing: f32, row_spacing: f32, + tree: &mut [Tree], ) -> Node { let max_size = limits.max(); @@ -33,10 +35,10 @@ pub fn resolve( let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); // Attach widgets as child nodes. - for (child, assignment) in items.iter().zip(assignments.iter()) { + for ((child, assignment), tree) in items.iter().zip(assignments.iter()).zip(tree.iter_mut()) { // Calculate the dimensions of the item. let child_widget = child.as_widget(); - let child_node = child_widget.layout(renderer, limits); + let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); nodes.push(child_node); @@ -155,12 +157,18 @@ pub fn resolve( } }; - for (leaf, (child, node)) in leafs.into_iter().zip(items.iter().zip(nodes.iter_mut())) { + for (((leaf, child), node), tree) in leafs + .into_iter() + .zip(items.iter()) + .zip(nodes.iter_mut()) + .zip(tree) + { if let Ok(leaf_layout) = taffy.layout(leaf) { let child_widget = child.as_widget(); match child_widget.width() { Length::Fill | Length::FillPortion(_) => { - *node = child_widget.layout(renderer, &limits.width(leaf_layout.size.width)); + *node = + child_widget.layout(tree, renderer, &limits.width(leaf_layout.size.width)); } _ => (), } diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 9a8a70c6..782738fb 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -120,7 +120,12 @@ impl<'a, Message: 'static + Clone> Widget for Grid<'a, Messag self.height } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits .max_width(self.max_width) .width(self.width()) @@ -138,6 +143,7 @@ impl<'a, Message: 'static + Clone> Widget for Grid<'a, Messag self.row_alignment, f32::from(self.column_spacing), f32::from(self.row_spacing), + &mut tree.children, ) } diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index ecff8dab..17c7e104 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -1,5 +1,6 @@ // From iced_aw, license MIT +use iced_core::widget::Tree; use iced_widget::core::{ layout::{Limits, Node}, renderer, Alignment, Element, Padding, Point, Size, @@ -54,6 +55,7 @@ pub fn resolve<'a, E, Message, Renderer>( spacing: f32, align_items: Alignment, items: &[E], + tree: &mut [Tree], ) -> Node where E: std::borrow::Borrow>, @@ -73,7 +75,7 @@ where if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for child in items { + for (child, tree) in items.iter().zip(tree.iter_mut()) { let child = child.borrow(); let cross_fill_factor = match axis { Axis::Horizontal => child.as_widget().height(), @@ -86,7 +88,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.as_widget().layout(renderer, &child_limits); + let layout = child.as_widget().layout(tree, renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -96,7 +98,7 @@ where cross = fill_cross; } - for (i, child) in items.iter().enumerate() { + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { let child = child.borrow(); let fill_factor = match axis { Axis::Horizontal => child.as_widget().width(), @@ -122,7 +124,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(renderer, &child_limits); + let layout = child.as_widget().layout(tree, renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -139,7 +141,7 @@ where let remaining = available.max(0.0); - for (i, child) in items.iter().enumerate() { + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { let child = child.borrow(); let fill_factor = match axis { Axis::Horizontal => child.as_widget().width(), @@ -172,7 +174,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(renderer, &child_limits); + let layout = child.as_widget().layout(tree, renderer, &child_limits); if align_items != Alignment::Center { cross = cross.max(axis.cross(layout.size())); diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 308b142e..a2bcdd3d 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -279,7 +279,7 @@ where .collect() } - fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { + fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); @@ -296,6 +296,8 @@ where self.spacing, Alignment::Center, &children, + // the children of the tree are the menu roots + &mut tree.children, ) } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 63629ae4..3608cf33 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -252,8 +252,13 @@ impl MenuBounds { where Renderer: renderer::Renderer, { - let (children_size, child_positions, child_sizes) = - get_children_layout(menu_tree, renderer, item_width, item_height); + let (children_size, child_positions, child_sizes) = get_children_layout( + menu_tree, + renderer, + item_width, + item_height, + &mut Tree::new(&menu_tree.item), + ); // viewport space parent bounds let view_parent_bounds = parent_bounds + overlay_offset; @@ -299,6 +304,7 @@ impl MenuState { slice: MenuSlice, renderer: &Renderer, menu_tree: &MenuTree<'_, Message, Renderer>, + tree: &mut [Tree], ) -> Node where Renderer: renderer::Renderer, @@ -322,7 +328,8 @@ impl MenuState { .iter() .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) .zip(menu_tree.children[start_index..=end_index].iter()) - .map(|((cp, size), mt)| { + .zip(tree[start_index..=end_index].iter_mut()) + .map(|(((cp, size), mt), tree)| { let mut position = *cp; let mut size = *size; @@ -336,7 +343,7 @@ impl MenuState { let limits = Limits::new(Size::ZERO, size); - let mut node = mt.item.as_widget().layout(renderer, &limits); + let mut node = mt.item.as_widget().layout(tree, renderer, &limits); node.move_to(Point::new(0.0, position + self.scroll_offset)); node }) @@ -353,6 +360,7 @@ impl MenuState { index: usize, renderer: &Renderer, menu_tree: &MenuTree<'_, Message, Renderer>, + tree: &mut Tree, ) -> Node where Renderer: renderer::Renderer, @@ -363,7 +371,7 @@ impl MenuState { let position = self.menu_bounds.child_positions[index]; let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]); let parent_offset = children_bounds.position() - Point::ORIGIN; - let mut node = menu_tree.item.as_widget().layout(renderer, &limits); + let mut node = menu_tree.item.as_widget().layout(tree, renderer, &limits); node.move_to(Point::new( parent_offset.x, parent_offset.y + position + self.scroll_offset, @@ -458,9 +466,44 @@ where Renderer: renderer::Renderer, Renderer::Theme: StyleSheet, { - fn layout(&self, _renderer: &Renderer, bounds: Size, position: Point) -> Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size, position: Point) -> Node { + // layout children + let state = self.tree.state.downcast_mut::(); + let overlay_offset = Point::ORIGIN - position; + let tree_children = &mut self.tree.children; + let children = state + .active_root + .map(|active_root| { + let root = &self.menu_roots[active_root]; + let active_tree = &mut tree_children[active_root]; + state.menu_states.iter().enumerate().fold( + (root, Vec::new()), + |(menu_root, mut nodes), (i, ms)| { + let slice = ms.slice(bounds, overlay_offset, self.item_height); + let start_index = slice.start_index; + let end_index = slice.end_index; + let children_node = ms.layout( + overlay_offset, + slice, + renderer, + menu_root, + &mut active_tree.children[start_index..=end_index], + ); + nodes.push(children_node); + // only the last menu can have a None active index + ( + ms.index + .map_or(menu_root, |active| &menu_root.children[active]), + nodes, + ) + }, + ) + }) + .map(|(_, l)| l) + .unwrap_or_default(); + // overlay space viewport rectangle - Node::new(bounds).translate(Point::ORIGIN - position) + Node::with_children(bounds, children).translate(Point::ORIGIN - position) } fn on_event( @@ -610,8 +653,9 @@ where state .menu_states .iter() + .zip(layout.children()) .enumerate() - .fold(root, |menu_root, (i, ms)| { + .fold(root, |menu_root, (i, (ms, children_layout))| { let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph { PathHighlight::Full => true, PathHighlight::OmitActive => !indices.is_empty() && i < indices.len() - 1, @@ -631,9 +675,6 @@ where let start_index = slice.start_index; let end_index = slice.end_index; - // calc layout - let children_node = ms.layout(overlay_offset, slice, r, menu_root); - let children_layout = Layout::new(&children_node); let children_bounds = children_layout.bounds(); // draw menu background @@ -808,11 +849,17 @@ where // get layout let last_ms = &state.menu_states[indices.len() - 1]; + let last_tree = tree.children[active_root] + .children + .iter_mut() + .last() + .unwrap(); let child_node = last_ms.layout_single( overlay_offset, last_ms.index.expect("missing index within menu state."), renderer, mt, + last_tree, ); let child_layout = Layout::new(&child_node); @@ -1108,6 +1155,7 @@ fn get_children_layout( renderer: &Renderer, item_width: ItemWidth, item_height: ItemHeight, + tree: &mut Tree, ) -> (Size, Vec, Vec) where Renderer: renderer::Renderer, @@ -1130,13 +1178,15 @@ where ItemHeight::Dynamic(d) => menu_tree .children .iter() - .map(|mt| { + .zip(tree.children.iter_mut()) + .map(|(mt, tree)| { let w = mt.item.as_widget(); match w.height() { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w .layout( + tree, renderer, &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), ) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index b0871917..41a6e5c9 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -71,8 +71,14 @@ where self.content.as_widget().height() } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.content.as_widget().layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let tree = &mut tree.children[0]; + self.content.as_widget().layout(tree, renderer, limits) } fn operate( @@ -203,9 +209,13 @@ impl<'a, 'b, Message, Renderer> overlay::Overlay where Renderer: iced_core::Renderer, { - fn layout(&self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { let limits = layout::Limits::new(Size::UNIT, bounds); - let mut node = self.content.borrow().as_widget().layout(renderer, &limits); + let mut node = self + .content + .borrow() + .as_widget() + .layout(self.tree, renderer, &limits); if self.centered { // Position is set to the center bottom of the lower widget let width = node.size().width; diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 507e1d81..41563a95 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -195,8 +195,14 @@ where Widget::height(&self.container) } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { self.container.layout( + tree, renderer, if self.ignore_bounds { &layout::Limits::NONE diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 03d696b2..8fa28c7c 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -5,7 +5,7 @@ use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{SegmentedButton, SegmentedVariant}; +use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; @@ -61,9 +61,14 @@ where #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] - fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn variant_layout( + &self, + state: &mut LocalState, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits.width(self.width); - let (mut width, height) = self.max_button_dimensions(renderer, limits.max()); + let (mut width, height) = self.max_button_dimensions(state, renderer, limits.max()); let num = self.model.items.len(); let spacing = f32::from(self.spacing); diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 7a5f8cf3..827f6cf4 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -5,7 +5,7 @@ use super::model::{Model, Selectable}; use super::style::StyleSheet; -use super::widget::{SegmentedButton, SegmentedVariant}; +use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; @@ -62,9 +62,14 @@ where #[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_sign_loss)] - fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn variant_layout( + &self, + state: &mut LocalState, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { let limits = limits.width(self.width); - let (width, mut height) = self.max_button_dimensions(renderer, limits.max()); + let (width, mut height) = self.max_button_dimensions(state, renderer, limits.max()); let num = self.model.items.len(); let spacing = f32::from(self.spacing); diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index bb22a516..ae4fd78d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -10,15 +10,16 @@ use iced::{ alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle, Size, }; -use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping}; +use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; use iced_core::widget::{self, operation, tree}; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; -use iced_core::{BorderRadius, Point, Renderer as IcedRenderer}; +use iced_core::{BorderRadius, Point, Renderer as IcedRenderer, Text}; +use slotmap::SecondaryMap; use std::marker::PhantomData; /// State that is maintained by each individual widget. #[derive(Default)] -struct LocalState { +pub struct LocalState { /// The first focusable key. first: Entity, /// If the widget is focused or not. @@ -27,6 +28,8 @@ struct LocalState { focused_key: Entity, /// The ID of the button that is being hovered. Defaults to null. hovered: Entity, + /// The paragraphs for each text. + paragraphs: SecondaryMap, } impl operation::Focusable for LocalState { @@ -57,7 +60,12 @@ pub trait SegmentedVariant { fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle; /// Calculates the layout of this variant. - fn variant_layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node; + fn variant_layout( + &self, + state: &mut LocalState, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node; } /// A conjoined group of items that function together as a button. @@ -214,7 +222,12 @@ where event::Status::Ignored } - pub(super) fn max_button_dimensions(&self, renderer: &Renderer, bounds: Size) -> (f32, f32) { + pub(super) fn max_button_dimensions( + &self, + state: &mut LocalState, + renderer: &Renderer, + bounds: Size, + ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; let font = renderer.default_font(); @@ -224,15 +237,21 @@ where let mut button_height = 0.0f32; // Add text to measurement if text was given. - if let Some(text) = self.model.text(key) { - let Size { width, height } = renderer.measure( - text, - self.font_size, - self.line_height, - font, - bounds, - Shaping::Advanced, - ); + if let Some((text, entry)) = self.model.text.get(key).zip(state.paragraphs.entry(key)) { + let paragraph = entry.or_insert_with(|| { + crate::Paragraph::with_text(Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }) + }); + + let Size { width, height } = paragraph.min_bounds(); button_width = width; button_height = height; @@ -282,12 +301,44 @@ where } fn state(&self) -> tree::State { + // update the paragraphs for the model tree::State::new(LocalState { first: self.model.order.iter().copied().next().unwrap_or_default(), + paragraphs: SecondaryMap::new(), ..LocalState::default() }) } + fn diff(&mut self, tree: &mut Tree) { + for e in self.model.order.iter().copied() { + if let Some(text) = self.model.text.get(e) { + let text = Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font: self.font_active.unwrap_or(crate::font::FONT), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }; + if let Some(paragraph) = tree + .state + .downcast_mut::() + .paragraphs + .get_mut(e) + { + paragraph.update(text); + } else { + tree.state + .downcast_mut::() + .paragraphs + .insert(e, crate::Paragraph::with_text(text)); + } + } + } + } + fn width(&self) -> Length { self.width } @@ -296,8 +347,13 @@ where self.height } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { - self.variant_layout(renderer, limits) + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.variant_layout(tree.state.downcast_mut::(), renderer, limits) } fn on_event( @@ -548,7 +604,7 @@ where }); Widget::::draw( - &Element::::from(icon.clone()), + Element::::from(icon.clone()).as_widget(), &Tree::empty(), renderer, theme, @@ -575,17 +631,20 @@ where bounds.y = y; // Draw the text in this button. - renderer.fill_text(iced_core::text::Text { - content: text, - size: self.font_size, - bounds, - color: status_appearance.text_color, - font, - horizontal_alignment, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - line_height: self.line_height, - }); + renderer.fill_text( + iced_core::text::Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: bounds.size(), + font, + horizontal_alignment, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }, + bounds.position(), + status_appearance.text_color, + ); } let show_close_button = diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 8930a55d..03286cba 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -5,6 +5,8 @@ //! Display fields that can be filled with text. //! //! A [`TextInput`] has some local [`State`]. +use std::borrow::Cow; + use crate::theme::THEME; use super::cursor; @@ -20,7 +22,7 @@ use iced_core::keyboard; use iced_core::mouse::{self, click}; use iced_core::overlay::Group; use iced_core::renderer::{self, Renderer as CoreRenderer}; -use iced_core::text::{self, Renderer, Text}; +use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::touch; use iced_core::widget::operation::{self, Operation}; @@ -48,7 +50,10 @@ use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMim /// Creates a new [`TextInput`]. /// /// [`TextInput`]: widget::TextInput -pub fn text_input<'a, Message>(placeholder: &str, value: &str) -> TextInput<'a, Message> +pub fn text_input<'a, Message>( + placeholder: impl Into>, + value: impl Into>, +) -> TextInput<'a, Message> where Message: Clone + 'static, { @@ -58,7 +63,10 @@ where /// Creates a new search [`TextInput`]. /// /// [`TextInput`]: widget::TextInput -pub fn search_input<'a, Message>(placeholder: &str, value: &str) -> TextInput<'a, Message> +pub fn search_input<'a, Message>( + placeholder: impl Into>, + value: impl Into>, +) -> TextInput<'a, Message> where Message: Clone + 'static, { @@ -79,8 +87,8 @@ where /// /// [`TextInput`]: widget::TextInput pub fn secure_input<'a, Message>( - placeholder: &str, - value: &str, + placeholder: impl Into>, + value: impl Into>, on_visible_toggle: Option, hidden: bool, ) -> TextInput<'a, Message> @@ -119,7 +127,7 @@ where /// Creates a new inline [`TextInput`]. /// /// [`TextInput`]: widget::TextInput -pub fn inline_input<'a, Message>(value: &str) -> TextInput<'a, Message> +pub fn inline_input<'a, Message>(value: impl Into>) -> TextInput<'a, Message> where Message: Clone + 'static, { @@ -171,7 +179,7 @@ pub type DnDCommand = (); #[must_use] pub struct TextInput<'a, Message> { id: Option, - placeholder: String, + placeholder: Cow<'a, str>, value: Value, is_secure: bool, font: Option<::Font>, @@ -206,13 +214,14 @@ where /// It expects: /// - a placeholder, /// - the current value - pub fn new(placeholder: &str, value: &str) -> Self { + pub fn new(placeholder: impl Into>, value: impl Into>) -> Self { let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); + let v: Cow<'a, str> = value.into(); TextInput { id: None, - placeholder: String::from(placeholder), - value: Value::new(value), + placeholder: placeholder.into(), + value: Value::new(v.as_ref()), is_secure: false, font: None, width: Length::Fill, @@ -474,6 +483,22 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); + // TODO get values from renderer somehow. + replace_paragraph( + state, + Layout::new(&layout::Node::with_children( + Size::INFINITY, + vec![layout::Node::with_children( + Size::INFINITY, + vec![layout::Node::default()], + )], + )), + &self.value, + self.font.unwrap_or(crate::font::FONT), + Pixels(self.size.unwrap_or(14.0)), + self.line_height, + ); + // Unfocus text input if it becomes disabled if self.on_input.is_none() { state.last_click = None; @@ -506,23 +531,39 @@ where Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let font = self.font.unwrap_or_else(|| renderer.default_font()); if self.dnd_icon { let limits = limits.width(Length::Shrink).height(Length::Shrink); - let size = self.size.unwrap_or_else(|| renderer.default_size()); + let size = self.size.unwrap_or_else(|| renderer.default_size().0); let bounds = limits.max(); - let font = self.font.unwrap_or_else(|| renderer.default_font()); - let Size { width, height } = renderer.measure( - &self.value.to_string(), - size, - self.line_height, + let state = tree.state.downcast_mut::(); + let value_paragraph = &mut state.value; + let v = self.value.to_string(); + value_paragraph.update(Text { + content: if self.value.is_empty() { + &self.placeholder + } else { + &v + }, font, bounds, - text::Shaping::Advanced, - ); + size: iced::Pixels(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Advanced, + }); + + let Size { width, height } = limits.resolve(value_paragraph.min_bounds()); let size = limits.resolve(Size::new(width, height)); layout::Node::with_children(size, vec![layout::Node::new(size)]) @@ -540,6 +581,8 @@ where self.helper_text, self.helper_size, self.helper_line_height, + font, + tree, ) } } @@ -697,6 +740,7 @@ where self.on_dnd_command_produced.as_deref(), self.surface_ids, self.line_height, + layout, ) } @@ -847,6 +891,8 @@ pub fn layout( helper_text: Option<&str>, helper_text_size: f32, helper_text_line_height: text::LineHeight, + font: iced_core::Font, + tree: &mut Tree, ) -> layout::Node { let limits = limits.width(width); let spacing = THEME.with(|t| t.borrow().cosmic().space_xxs()); @@ -854,15 +900,19 @@ pub fn layout( let text_pos = if let Some(label) = label { let text_bounds = limits.resolve(Size::ZERO); - - let label_size = renderer.measure( - label, - size.unwrap_or_else(|| renderer.default_size()), + let state = tree.state.downcast_mut::(); + let label_paragraph = &mut state.label; + label_paragraph.update(Text { + content: label, + font, + bounds: text_bounds, + size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, line_height, - renderer.default_font(), - text_bounds, - text::Shaping::Advanced, - ); + shaping: text::Shaping::Advanced, + }); + let label_size = label_paragraph.min_bounds(); nodes.push(layout::Node::new(label_size)); Vector::new(0.0, label_size.height + f32::from(spacing)) @@ -870,41 +920,48 @@ pub fn layout( Vector::ZERO }; - let text_size = size.unwrap_or_else(|| renderer.default_size()); + let text_size = size.unwrap_or_else(|| renderer.default_size().0); let mut text_input_height = text_size * 1.2; let padding = padding.fit(Size::ZERO, limits.max()); let helper_pos = if leading_icon.is_some() || trailing_icon.is_some() { + let children = &mut tree.children; // TODO configurable icon spacing, maybe via appearance let limits_copy = limits; let limits = limits.pad(padding); let icon_spacing = 8.0; - let (leading_icon_width, mut leading_icon) = if let Some(icon) = leading_icon.as_ref() { - let icon_node = icon.layout( - renderer, - &Limits::NONE - .width(icon.as_widget().width()) - .height(icon.as_widget().height()), - ); - text_input_height = text_input_height.max(icon_node.bounds().height); - (icon_node.bounds().width + icon_spacing, Some(icon_node)) - } else { - (0.0, None) - }; + let mut c_i = 0; + let (leading_icon_width, mut leading_icon) = + if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) { + let icon_node = icon.as_widget().layout( + tree, + renderer, + &Limits::NONE + .width(icon.as_widget().width()) + .height(icon.as_widget().height()), + ); + text_input_height = text_input_height.max(icon_node.bounds().height); + c_i += 1; + (icon_node.bounds().width + icon_spacing, Some(icon_node)) + } else { + (0.0, None) + }; - let (trailing_icon_width, mut trailing_icon) = if let Some(icon) = trailing_icon.as_ref() { - let icon_node = icon.layout( - renderer, - &Limits::NONE - .width(icon.as_widget().width()) - .height(icon.as_widget().height()), - ); - text_input_height = text_input_height.max(icon_node.bounds().height); - (icon_node.bounds().width + icon_spacing, Some(icon_node)) - } else { - (0.0, None) - }; + let (trailing_icon_width, mut trailing_icon) = + if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) { + let icon_node = icon.layout( + tree, + renderer, + &Limits::NONE + .width(icon.as_widget().width()) + .height(icon.as_widget().height()), + ); + text_input_height = text_input_height.max(icon_node.bounds().height); + (icon_node.bounds().width + icon_spacing, Some(icon_node)) + } else { + (0.0, None) + }; let text_limits = limits.width(width).height(text_size * 1.2); let text_bounds = text_limits.resolve(Size::ZERO); @@ -978,14 +1035,19 @@ pub fn layout( .height(helper_text_size * 1.2); let text_bounds = limits.resolve(Size::ZERO); - let helper_text_size = renderer.measure( - helper_text, - helper_text_size, - helper_text_line_height, - renderer.default_font(), - text_bounds, - text::Shaping::Advanced, - ); + let state = tree.state.downcast_mut::(); + let helper_text_paragraph = &mut state.label; + helper_text_paragraph.update(Text { + content: helper_text, + font, + bounds: text_bounds, + size: iced::Pixels(helper_text_size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: helper_text_line_height, + shaping: text::Shaping::Advanced, + }); + let helper_text_size = helper_text_paragraph.min_bounds(); nodes.push(layout::Node::new(helper_text_size).translate(helper_pos)); }; @@ -1013,16 +1075,16 @@ pub fn layout( #[allow(clippy::missing_panics_doc)] #[allow(clippy::cast_lossless)] #[allow(clippy::cast_possible_truncation)] -pub fn update<'a, Message, Renderer>( +pub fn update<'a, Message>( event: Event, text_layout: Layout<'_>, cursor_position: mouse::Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, value: &mut Value, size: Option, - font: Option, + font: Option<::Font>, is_secure: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, @@ -1033,11 +1095,17 @@ pub fn update<'a, Message, Renderer>( on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>, surface_ids: Option<(window::Id, window::Id)>, line_height: text::LineHeight, + layout: Layout<'_>, ) -> event::Status where Message: Clone, - Renderer: text::Renderer, { + let font = font.unwrap_or_else(|| renderer.default_font()); + let size = size.unwrap_or_else(|| renderer.default_size().0); + let update_cache = |state, value| { + replace_paragraph(state, layout, value, font, iced::Pixels(size), line_height); + }; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -1056,9 +1124,6 @@ where None }; - let font: ::Font = - font.unwrap_or_else(|| renderer.default_font()); - if is_clicked { let Some(pos) = cursor_position.position() else { return event::Status::Ignored; @@ -1092,27 +1157,19 @@ where surface_ids, on_input, ) { - let actual_size = size.unwrap_or_else(|| renderer.default_size()); - let left = start.min(end); let right = end.max(start); let (left_position, _left_offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_layout.bounds(), - value, - actual_size, left, - font, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( - renderer, + &state.value, text_layout.bounds(), - value, - actual_size, right, - font, ); let width = right_position - left_position; @@ -1160,14 +1217,10 @@ where }; find_cursor_position( - renderer, text_layout.bounds(), - font, - size, &value, - state, + &state, target, - line_height, ) } else { None @@ -1189,16 +1242,7 @@ where value.clone() }; - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) + find_cursor_position(text_layout.bounds(), &value, state, target) } else { None }; @@ -1210,17 +1254,9 @@ where if is_secure { state.cursor.select_all(value); } else { - let position = find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - value, - state, - target, - line_height, - ) - .unwrap_or(0); + let position = + find_cursor_position(text_layout.bounds(), value, state, target) + .unwrap_or(0); state.cursor.select_range( value.previous_start_of_word(position), @@ -1260,20 +1296,8 @@ where } else { value.clone() }; - let font: ::Font = - font.unwrap_or_else(|| renderer.default_font()); - - let position = find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) - .unwrap_or(0); + let position = + find_cursor_position(text_layout.bounds(), &value, state, target).unwrap_or(0); state .cursor @@ -1303,6 +1327,8 @@ where focus.updated_at = Instant::now(); + update_cache(state, value); + return event::Status::Captured; } } @@ -1341,6 +1367,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::Delete => { if platform::is_jump_modifier_pressed(modifiers) @@ -1359,6 +1387,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -1417,6 +1447,8 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + + update_cache(state, value); } keyboard::KeyCode::V => { if state.keyboard_modifiers.command() { @@ -1445,6 +1477,8 @@ where shell.publish(message); state.is_pasting = Some(content); + + update_cache(state, value); } else { state.is_pasting = None; } @@ -1571,18 +1605,7 @@ where value.clone() }; - let font = font.unwrap_or_else(|| renderer.default_font()); - - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) + find_cursor_position(text_layout.bounds(), &value, state, target) } else { None }; @@ -1651,18 +1674,8 @@ where } else { value.clone() }; - let font = font.unwrap_or_else(|| renderer.default_font()); - find_cursor_position( - renderer, - text_layout.bounds(), - font, - size, - &value, - state, - target, - line_height, - ) + find_cursor_position(text_layout.bounds(), &value, state, target) } else { None }; @@ -1895,17 +1908,20 @@ pub fn draw<'a, Message>( // draw the label if it exists if let (Some(label_layout), Some(label)) = (label_layout, label) { - renderer.fill_text(Text { - content: label, - size: size.unwrap_or_else(|| renderer.default_size()), - font: font.unwrap_or_else(|| renderer.default_font()), - color: appearance.label_color, - bounds: label_layout.bounds(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - line_height, - shaping: text::Shaping::Advanced, - }); + renderer.fill_text( + Text { + content: label, + size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), + font: font.unwrap_or_else(|| renderer.default_font()), + bounds: label_layout.bounds().size(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height, + shaping: text::Shaping::Advanced, + }, + bounds.position(), + appearance.label_color, + ); } let mut child_index = 0; let leading_icon_tree = children.get(child_index); @@ -1931,19 +1947,13 @@ pub fn draw<'a, Message>( let text = value.to_string(); let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size()); + let size = size.unwrap_or_else(|| renderer.default_size().0); let (cursor, offset) = if let Some(focus) = &state.is_focused { match state.cursor.state(value) { cursor::State::Index(position) => { - let (text_value_width, offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - position, - font, - ); + let (text_value_width, offset) = + measure_cursor_and_scroll_offset(&state.value, text_bounds, position); let is_cursor_visible = ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) % 2 @@ -1979,23 +1989,12 @@ pub fn draw<'a, Message>( let left = start.min(end); let right = end.max(start); - let (left_position, left_offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - left, - font, - ); + let value_paragraph = &state.value; + let (left_position, left_offset) = + measure_cursor_and_scroll_offset(value_paragraph, text_bounds, left); - let (right_position, right_offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - right, - font, - ); + let (right_position, right_offset) = + measure_cursor_and_scroll_offset(value_paragraph, text_bounds, right); let width = right_position - left_position; @@ -2030,12 +2029,7 @@ pub fn draw<'a, Message>( (None, 0.0) }; - let text_width = renderer.measure_width( - if text.is_empty() { placeholder } else { &text }, - size, - font, - text::Shaping::Advanced, - ); + let text_width = state.value.min_width(); let render = |renderer: &mut crate::Renderer| { if let Some((cursor, color)) = cursor { @@ -2044,25 +2038,30 @@ pub fn draw<'a, Message>( renderer.with_translation(Vector::ZERO, |_| {}); } - renderer.fill_text(Text { - content: if text.is_empty() { placeholder } else { &text }, - color: if text.is_empty() { - appearance.placeholder_color - } else { - appearance.text_color + let bounds = Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }; + let color = if text.is_empty() { + appearance.placeholder_color + } else { + appearance.text_color + }; + renderer.fill_text( + Text { + content: if text.is_empty() { placeholder } else { &text }, + font, + bounds: bounds.size(), + size: iced::Pixels(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + line_height: text::LineHeight::default(), + shaping: text::Shaping::Advanced, }, - font, - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - line_height: text::LineHeight::default(), - shaping: text::Shaping::Advanced, - }); + bounds.position(), + color, + ); }; if text_width > text_bounds.width { @@ -2096,17 +2095,20 @@ pub fn draw<'a, Message>( // draw the helper text if it exists if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) { - renderer.fill_text(Text { - content: helper_text, - size: helper_text_size, - font, - color: appearance.text_color, - bounds: helper_text_layout.bounds(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - line_height: helper_line_height, - shaping: text::Shaping::Advanced, - }); + renderer.fill_text( + Text { + content: helper_text, + size: iced::Pixels(helper_text_size), + font, + bounds: helper_text_layout.bounds().size(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + line_height: helper_line_height, + shaping: text::Shaping::Advanced, + }, + helper_text_layout.bounds().position(), + appearance.text_color, + ); } } @@ -2165,6 +2167,9 @@ pub(crate) struct DndOfferState; #[derive(Debug, Default, Clone)] #[must_use] pub struct State { + pub value: crate::Paragraph, + pub placeholder: crate::Paragraph, + pub label: crate::Paragraph, is_focused: Option, dragging_state: Option, dnd_offer: DndOfferState, @@ -2214,6 +2219,10 @@ impl State { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused() -> Self { Self { + value: crate::Paragraph::new(), + placeholder: crate::Paragraph::new(), + label: crate::Paragraph::new(), + is_focused: None, dragging_state: None, #[allow(clippy::default_constructed_unit_structs)] @@ -2307,6 +2316,70 @@ impl operation::TextInput for State { } } +fn measure_cursor_and_scroll_offset( + paragraph: &impl text::Paragraph, + text_bounds: Rectangle, + cursor_index: usize, +) -> (f32, f32) { + let grapheme_position = paragraph + .grapheme_position(0, cursor_index) + .unwrap_or(Point::ORIGIN); + + let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); + + (grapheme_position.x, offset) +} + +/// Computes the position of the text cursor at the given X coordinate of +/// a [`TextInput`]. +fn find_cursor_position( + text_bounds: Rectangle, + value: &Value, + state: &State, + x: f32, +) -> Option { + let offset = offset(text_bounds, value, state); + let value = value.to_string(); + + let char_offset = state + .value + .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) + .map(text::Hit::cursor)?; + + Some( + unicode_segmentation::UnicodeSegmentation::graphemes( + &value[..char_offset.min(value.len())], + true, + ) + .count(), + ) +} + +fn replace_paragraph( + state: &mut State, + layout: Layout<'_>, + value: &Value, + font: ::Font, + text_size: Pixels, + line_height: text::LineHeight, +) { + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + state.value = crate::Paragraph::with_text(Text { + font, + line_height, + content: &value.to_string(), + bounds: Size::new(f32::INFINITY, text_bounds.height), + size: text_size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); +} + +const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + mod platform { use iced_core::keyboard; @@ -2319,17 +2392,7 @@ mod platform { } } -fn offset( - renderer: &Renderer, - text_bounds: Rectangle, - font: Renderer::Font, - size: f32, - value: &Value, - state: &State, -) -> f32 -where - Renderer: text::Renderer, -{ +fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { if state.is_focused() { let cursor = state.cursor(); @@ -2338,77 +2401,11 @@ where cursor::State::Selection { end, .. } => end, }; - let (_, offset) = measure_cursor_and_scroll_offset( - renderer, - text_bounds, - value, - size, - focus_position, - font, - ); + let (_, offset) = + measure_cursor_and_scroll_offset(&state.value, text_bounds, focus_position); offset } else { 0.0 } } - -fn measure_cursor_and_scroll_offset( - renderer: &Renderer, - text_bounds: Rectangle, - value: &Value, - size: f32, - cursor_index: usize, - font: Renderer::Font, -) -> (f32, f32) -where - Renderer: text::Renderer, -{ - let text_before_cursor = value.until(cursor_index).to_string(); - - let text_value_width = - renderer.measure_width(&text_before_cursor, size, font, text::Shaping::Advanced); - - let offset = ((text_value_width + 5.0) - text_bounds.width).max(0.0); - - (text_value_width, offset) -} - -/// Computes the position of the text cursor at the given X coordinate of -/// a [`TextInput`]. -#[allow(clippy::too_many_arguments)] -fn find_cursor_position( - renderer: &Renderer, - text_bounds: Rectangle, - font: Renderer::Font, - size: Option, - value: &Value, - state: &State, - x: f32, - line_height: text::LineHeight, -) -> Option -where - Renderer: text::Renderer, -{ - let size = size.unwrap_or_else(|| renderer.default_size()); - - let offset = offset(renderer, text_bounds, font, size, value, state); - let value = value.to_string(); - - let char_offset = renderer - .hit_test( - &value, - size, - line_height, - font, - Size::INFINITY, - text::Shaping::Advanced, - Point::new(x + offset, text_bounds.height / 2.0), - true, - ) - .map(text::Hit::cursor)?; - - Some(unicode_segmentation::UnicodeSegmentation::graphemes(&value[..char_offset], true).count()) -} - -const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; From 225bbabe34f38fd845df04936bdbe1b04f97e2bd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 17:21:56 -0500 Subject: [PATCH 0002/1050] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 9af6bbb5..14ef12f4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 9af6bbb55c3443a21b835b01f20b2c0032cb50bc +Subproject commit 14ef12f429078be78c45cd3348162b5f8459e993 From 5f2e83b04d84aacc346cb7ee75c084e7e6fa308e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 30 Nov 2023 17:47:39 -0500 Subject: [PATCH 0003/1050] fix: avoid text_input paragraph updates in diff --- src/widget/text_input/input.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 03286cba..993dd987 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -483,22 +483,6 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); - // TODO get values from renderer somehow. - replace_paragraph( - state, - Layout::new(&layout::Node::with_children( - Size::INFINITY, - vec![layout::Node::with_children( - Size::INFINITY, - vec![layout::Node::default()], - )], - )), - &self.value, - self.font.unwrap_or(crate::font::FONT), - Pixels(self.size.unwrap_or(14.0)), - self.line_height, - ); - // Unfocus text input if it becomes disabled if self.on_input.is_none() { state.last_click = None; From 0e9b46af724ac24b4613b96f05d340c820a89e95 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 1 Dec 2023 14:18:53 -0500 Subject: [PATCH 0004/1050] update for multi-menu --- iced | 2 +- src/widget/dropdown/multi/menu.rs | 60 +++++++----- src/widget/dropdown/multi/widget.rs | 143 ++++++++++++++++++---------- 3 files changed, 133 insertions(+), 72 deletions(-) diff --git a/iced b/iced index 14ef12f4..d67f1a1c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 14ef12f429078be78c45cd3348162b5f8459e993 +Subproject commit d67f1a1c79cf3dcb378520ef7e4e9ab653f75bea diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index cd510ddc..aac00107 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -190,7 +190,12 @@ impl<'a, Message: 'a> Overlay<'a, Message> { } impl<'a, Message> iced_core::Overlay for Overlay<'a, Message> { - fn layout(&self, renderer: &crate::Renderer, bounds: Size, position: Point) -> layout::Node { + fn layout( + &mut self, + renderer: &crate::Renderer, + bounds: Size, + position: Point, + ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -207,7 +212,9 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M ) .width(self.width); - let mut node = self.container.layout(renderer, &limits); + let mut node = self + .container + .layout(&mut self.state.children[0], renderer, &limits); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) @@ -296,13 +303,18 @@ where Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + _tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { use std::f32; let limits = limits.width(Length::Fill).height(Length::Shrink); let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let text_line_height = self.text_line_height.to_absolute(Pixels(text_size)); @@ -359,7 +371,7 @@ where if let Some(cursor_position) = cursor.position_in(bounds) { let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))); @@ -406,7 +418,7 @@ where if let Some(cursor_position) = cursor.position_in(bounds) { let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))); @@ -488,7 +500,7 @@ where let text_size = self .text_size - .unwrap_or_else(|| text::Renderer::default_size(renderer)); + .unwrap_or_else(|| text::Renderer::default_size(renderer).0); let offset = viewport.y - bounds.y; @@ -571,24 +583,26 @@ where (appearance.text_color, crate::font::FONT) }; + let bounds = Rectangle { + x: bounds.x + self.padding.left, + y: bounds.y + self.padding.top, + width: bounds.width, + height: bounds.height, + }; text::Renderer::fill_text( renderer, Text { content: option.as_ref(), - bounds: Rectangle { - x: bounds.x + self.padding.left, - y: bounds.center_y(), - width: bounds.width, - ..bounds - }, - size: text_size, + bounds: bounds.size(), + size: iced::Pixels(text_size), line_height: self.text_line_height, font, - color, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + color, ); } @@ -618,23 +632,25 @@ where } OptionElement::Description(description) => { + let bounds = Rectangle { + x: bounds.center_x(), + y: bounds.center_y(), + ..bounds + }; text::Renderer::fill_text( renderer, Text { content: description.as_ref(), - bounds: Rectangle { - x: bounds.center_x(), - y: bounds.center_y(), - ..bounds - }, - size: text_size, + bounds: bounds.size(), + size: iced::Pixels(text_size), line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), font: crate::font::FONT, - color: appearance.description_color, horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + appearance.description_color, ); } } diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index df5d618b..3f59fcc2 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -6,7 +6,7 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; use iced_core::event::{self, Event}; -use iced_core::text::{self, Text}; +use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget}; @@ -78,7 +78,12 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> Length::Shrink } - fn layout(&self, renderer: &crate::Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { layout( renderer, limits, @@ -88,11 +93,16 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> self.text_size.unwrap_or(14.0), self.text_line_height, self.font, - self.selections - .selected - .as_ref() - .and_then(|id| self.selections.get(id)) - .map(AsRef::as_ref), + self.selections.selected.as_ref().and_then(|id| { + self.selections.get(id).map(AsRef::as_ref).zip( + tree.state + .downcast_mut::>() + .selections + .iter_mut() + .find(|(i, _)| i == id) + .map(|(_, p)| p), + ) + }), ) } @@ -177,6 +187,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> self.padding, self.text_size.unwrap_or(14.0), self.font, + self.text_line_height, self.selections, &self.on_selected, ) @@ -199,6 +210,8 @@ pub struct State { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, + selections: Vec<(Item, crate::Paragraph)>, + descriptions: Vec, } impl State { @@ -217,6 +230,8 @@ impl State { keyboard_modifiers: keyboard::Modifiers::default(), is_open: false, hovered_option: None, + selections: Vec::new(), + descriptions: Vec::new(), } } } @@ -238,7 +253,7 @@ pub fn layout( text_size: f32, text_line_height: text::LineHeight, font: Option, - selection: Option<&str>, + selection: Option<(&str, &mut crate::Paragraph)>, ) -> layout::Node { use std::f32; @@ -246,16 +261,18 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size, - font.unwrap_or_else(|| text::Renderer::default_font(renderer)), - text::Shaping::Advanced, - ); - - width.round() + let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 { + paragraph.update(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(|| text::Renderer::default_font(renderer)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + paragraph.min_width().round() }; selection.map(measure).unwrap_or_default() @@ -359,6 +376,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static padding: Padding, text_size: f32, font: Option, + text_line_height: text::LineHeight, selections: &'a super::Model, on_selected: &'a dyn Fn(Item) -> Message, ) -> Option> { @@ -378,38 +396,62 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static None, ) .width({ - let measure = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size, - crate::font::FONT, - text::Shaping::Advanced, - ); - - width.round() + let measure = |label: &str, paragraph: &mut crate::Paragraph| { + paragraph.update(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(|| text::Renderer::default_font(renderer)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + paragraph.min_width().round() }; - let measure_description = |label: &str| -> f32 { - let width = text::Renderer::measure_width( - renderer, - label, - text_size + 4.0, - crate::font::FONT, - text::Shaping::Advanced, - ); - - width.round() + let measure_description = |label: &str, paragraph: &mut crate::Paragraph| { + paragraph.update(Text { + content: label, + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size + 4.0), + line_height: text_line_height, + font: font.unwrap_or_else(|| text::Renderer::default_font(renderer)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + paragraph.min_width().round() }; + let mut desc_count = 0; selections .elements() .map(|element| match element { super::menu::OptionElement::Description(desc) => { - measure_description(desc.as_ref()) + let paragraph = if state.descriptions.len() > desc_count { + &mut state.descriptions[desc_count] + } else { + state.descriptions.push(crate::Paragraph::new()); + state.descriptions.last_mut().unwrap() + }; + desc_count += 1; + measure_description(desc.as_ref(), paragraph) } - super::menu::OptionElement::Option((option, _item)) => measure(option.as_ref()), + super::menu::OptionElement::Option((option, item)) => { + let paragraph = if let Some(index) = + state.selections.iter().position(|(i, _)| i == item) + { + &mut state.selections[index].1 + } else { + state + .selections + .push((item.clone(), crate::Paragraph::new())); + &mut state.selections.last_mut().unwrap().1 + }; + measure(option.as_ref(), paragraph) + } super::menu::OptionElement::Separator => 1.0, }) @@ -482,26 +524,29 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( } if let Some(content) = selected.map(AsRef::as_ref) { - let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer)); + let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); + + let bounds = Rectangle { + x: bounds.x + padding.left, + y: bounds.center_y(), + width: bounds.width - padding.horizontal(), + height: f32::from(text_line_height.to_absolute(Pixels(text_size))), + }; text::Renderer::fill_text( renderer, Text { content, - size: text_size, + size: iced::Pixels(text_size), line_height: text_line_height, font, - color: style.text_color, - bounds: Rectangle { - x: bounds.x + padding.left, - y: bounds.center_y(), - width: bounds.width - padding.horizontal(), - height: f32::from(text_line_height.to_absolute(Pixels(text_size))), - }, + bounds: bounds.size(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, }, + bounds.position(), + style.text_color, ); } } From 34e886e2d53551290f26f172c21f79edf921c8ee Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 1 Dec 2023 15:52:33 -0500 Subject: [PATCH 0005/1050] fix: specify that lineheight is absolute for heading --- src/widget/text.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/widget/text.rs b/src/widget/text.rs index 6712d84c..60a6cbd8 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -1,5 +1,6 @@ use crate::Renderer; pub use iced::widget::Text; +use iced_core::text::LineHeight; use std::borrow::Cow; /// Creates a new [`Text`] widget with the provided content. @@ -27,30 +28,36 @@ pub enum Typography { pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { Text::new(text) .size(32.0) - .line_height(44.0) + .line_height(LineHeight::Absolute(44.0.into())) .font(crate::font::FONT_LIGHT) } /// [`Text`] widget with the Title 2 typography preset. pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { - Text::new(text).size(28.0).line_height(36.0) + Text::new(text) + .size(28.0) + .line_height(LineHeight::Absolute(36.0.into())) } /// [`Text`] widget with the Title 3 typography preset. pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { - Text::new(text).size(24.0).line_height(32.0) + Text::new(text) + .size(24.0) + .line_height(LineHeight::Absolute(32.0.into())) } /// [`Text`] widget with the Title 4 typography preset. pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { - Text::new(text).size(20.0).line_height(28.0) + Text::new(text) + .size(20.0) + .line_height(LineHeight::Absolute(28.0.into())) } /// [`Text`] widget with the Heading typography preset. pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { Text::new(text) .size(14.0) - .line_height(20.0) + .line_height(LineHeight::Absolute(iced::Pixels(20.0))) .font(crate::font::FONT_SEMIBOLD) } @@ -58,24 +65,28 @@ pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { Text::new(text) .size(10.0) - .line_height(14.0) + .line_height(LineHeight::Absolute(iced::Pixels(14.0))) .font(crate::font::FONT_SEMIBOLD) } /// [`Text`] widget with the Body typography preset. pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { - Text::new(text).size(14.0).line_height(20.0) + Text::new(text) + .size(14.0) + .line_height(LineHeight::Absolute(20.0.into())) } /// [`Text`] widget with the Caption typography preset. pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { - Text::new(text).size(10.0).line_height(14.0) + Text::new(text) + .size(10.0) + .line_height(LineHeight::Absolute(14.0.into())) } /// [`Text`] widget with the Monotext typography preset. pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, Renderer> { Text::new(text) .size(14.0) - .line_height(20.0) + .line_height(LineHeight::Absolute(20.0.into())) .font(crate::font::FONT_MONO_REGULAR) } From 7d0ba4dba9651b79a3415b95439f927703ac5530 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 1 Dec 2023 16:24:37 -0500 Subject: [PATCH 0006/1050] fix: applets feature --- src/applet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 04c87b2a..e6913bb3 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -257,7 +257,7 @@ pub fn run(autosize: bool, flags: App::Flags) -> iced::Result iced.antialiasing = settings.antialiasing; iced.default_font = settings.default_font; - iced.default_text_size = settings.default_text_size; + iced.default_text_size = settings.default_text_size.into(); iced.id = Some(App::APP_ID.to_owned()); { From ad241c700a1caade6f1f4c86706649dac6c56071 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 3 Dec 2023 01:27:06 -0500 Subject: [PATCH 0007/1050] chore: implement text editor StyleSheet --- src/theme/style/iced.rs | 83 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 27635a5a..ba58c136 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -1084,3 +1084,86 @@ impl crate::widget::card::style::StyleSheet for Theme { } } } + +#[derive(Default)] +pub enum TextEditor { + #[default] + Default, + Custom(Box>), +} + +impl iced_style::text_editor::StyleSheet for Theme { + type Style = TextEditor; + + fn active(&self, style: &Self::Style) -> iced_style::text_editor::Appearance { + if let TextEditor::Custom(style) = style { + return style.active(self); + } + + let cosmic = self.cosmic(); + iced_style::text_editor::Appearance { + background: iced::Color::from(cosmic.bg_color()).into(), + border_radius: cosmic.corner_radii.radius_0.into(), + border_width: f32::from(cosmic.space_xxxs()), + border_color: iced::Color::from(cosmic.bg_divider()), + } + } + + fn focused(&self, style: &Self::Style) -> iced_style::text_editor::Appearance { + if let TextEditor::Custom(style) = style { + return style.focused(self); + } + + let cosmic = self.cosmic(); + iced_style::text_editor::Appearance { + background: iced::Color::from(cosmic.bg_color()).into(), + border_radius: cosmic.corner_radii.radius_0.into(), + border_width: f32::from(cosmic.space_xxxs()), + border_color: iced::Color::from(cosmic.accent.base), + } + } + + fn placeholder_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(style) = style { + return style.placeholder_color(self); + } + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.7; + neutral_9.into() + } + + fn value_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(style) = style { + return style.value_color(self); + } + let palette = self.cosmic(); + + palette.palette.neutral_9.into() + } + + fn disabled_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(style) = style { + return style.disabled_color(self); + } + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.5; + neutral_9.into() + } + + fn selection_color(&self, style: &Self::Style) -> Color { + if let TextEditor::Custom(style) = style { + return style.selection_color(self); + } + let cosmic = self.cosmic(); + cosmic.accent.base.into() + } + + fn disabled(&self, style: &Self::Style) -> iced_style::text_editor::Appearance { + if let TextEditor::Custom(style) = style { + return style.disabled(self); + } + self.active(style) + } +} From 17bc373990948a5c445f776fab11d3e62a126c20 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 4 Dec 2023 10:14:50 -0500 Subject: [PATCH 0008/1050] clippy fixes --- src/keyboard_nav.rs | 2 +- src/widget/dropdown/menu/mod.rs | 2 +- src/widget/dropdown/widget.rs | 8 ++++---- src/widget/flex_row/layout.rs | 2 +- src/widget/menu/menu_inner.rs | 2 +- src/widget/segmented_button/widget.rs | 2 +- src/widget/text_input/input.rs | 14 +++++++------- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 1f3e7ffd..32bea5b2 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -6,7 +6,7 @@ use iced::{ event, keyboard::{self, KeyCode}, - mouse, subscription, Command, Event, Subscription, + mouse, Command, Event, Subscription, }; use iced_core::{ widget::{operation, Id, Operation}, diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 589ff295..5c5f490b 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -209,7 +209,7 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M ) .width(self.width); - let mut node = self.container.layout(&mut self.state, renderer, &limits); + let mut node = self.container.layout(self.state, renderer, &limits); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 632eaf56..37306098 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -413,13 +413,13 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In #[allow(clippy::too_many_arguments)] pub fn overlay<'a, S: AsRef, Message: 'a>( layout: Layout<'_>, - renderer: &crate::Renderer, + _renderer: &crate::Renderer, state: &'a mut State, gap: f32, padding: Padding, text_size: f32, - text_line_height: text::LineHeight, - font: Option, + _text_line_height: text::LineHeight, + _font: Option, selections: &'a [S], selected_option: Option, on_selected: &'a dyn Fn(usize) -> Message, @@ -440,7 +440,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( None, ) .width({ - let measure = |label: &str, selection_paragraph: &mut crate::Paragraph| -> f32 { + let measure = |_label: &str, selection_paragraph: &mut crate::Paragraph| -> f32 { selection_paragraph.min_width().round() }; diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index ba4de64e..bcd9e4a9 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -28,7 +28,7 @@ pub fn resolve( let mut row_buffer = Vec::::with_capacity(8); - for (child, tree) in items.iter().zip(tree.into_iter()) { + for (child, tree) in items.iter().zip(tree.iter_mut()) { // Calculate the dimensions of the item. let child_node = child.as_widget().layout(tree, renderer, &limits); let size = child_node.size(); diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 3608cf33..4c748f30 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -478,7 +478,7 @@ where let active_tree = &mut tree_children[active_root]; state.menu_states.iter().enumerate().fold( (root, Vec::new()), - |(menu_root, mut nodes), (i, ms)| { + |(menu_root, mut nodes), (_i, ms)| { let slice = ms.slice(bounds, overlay_offset, self.item_height); let start_index = slice.start_index; let end_index = slice.end_index; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index ae4fd78d..d2847c43 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -226,7 +226,7 @@ where &self, state: &mut LocalState, renderer: &Renderer, - bounds: Size, + _bounds: Size, ) -> (f32, f32) { let mut width = 0.0f32; let mut height = 0.0f32; diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 993dd987..31468188 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -196,7 +196,6 @@ pub struct TextInput<'a, Message> { leading_icon: Option>, trailing_icon: Option>, style: <::Theme as StyleSheet>::Style, - // (text_input::State, mime_type, dnd_action) -> Message on_create_dnd_source: Option Message + 'a>>, on_dnd_command_produced: Option Message + 'a>>, surface_ids: Option<(window::Id, window::Id)>, @@ -1074,10 +1073,10 @@ pub fn update<'a, Message>( on_paste: Option<&dyn Fn(String) -> Message>, on_submit: &Option, state: impl FnOnce() -> &'a mut State, - on_start_dnd_source: Option<&dyn Fn(State) -> Message>, - _dnd_icon: bool, - on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>, - surface_ids: Option<(window::Id, window::Id)>, + #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, + #[allow(unused_variables)] dnd_icon: bool, + #[allow(unused_variables)] on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>, + #[allow(unused_variables)] surface_ids: Option<(window::Id, window::Id)>, line_height: text::LineHeight, layout: Layout<'_>, ) -> event::Status @@ -1203,7 +1202,7 @@ where find_cursor_position( text_layout.bounds(), &value, - &state, + state, target, ) } else { @@ -2156,6 +2155,7 @@ pub struct State { pub label: crate::Paragraph, is_focused: Option, dragging_state: Option, + #[cfg(feature = "wayland")] dnd_offer: DndOfferState, is_pasting: Option, last_click: Option, @@ -2209,7 +2209,7 @@ impl State { is_focused: None, dragging_state: None, - #[allow(clippy::default_constructed_unit_structs)] + #[cfg(feature = "wayland")] dnd_offer: DndOfferState::default(), is_pasting: None, last_click: None, From fcfe9ebc5978063c7ff9d4fb0f0b3f036c3f0b55 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 4 Dec 2023 16:49:14 -0500 Subject: [PATCH 0009/1050] chore: update iced --- examples/cosmic/src/window/demo.rs | 40 --------------------------- iced | 2 +- src/widget/context_drawer/overlay.rs | 1 + src/widget/dropdown/menu/mod.rs | 2 ++ src/widget/dropdown/multi/menu.rs | 3 ++ src/widget/dropdown/multi/widget.rs | 5 +++- src/widget/dropdown/widget.rs | 5 +++- src/widget/menu/menu_inner.rs | 8 +++++- src/widget/popover.rs | 8 +++++- src/widget/segmented_button/widget.rs | 1 + src/widget/text_input/input.rs | 3 ++ 11 files changed, 33 insertions(+), 45 deletions(-) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 824b243d..6329dd31 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -475,49 +475,9 @@ impl State { &self.entry_value, ) .on_input(Message::InputChanged) - // .on_submit(Message::Activate(None)) .size(20) .id(INPUT_ID.clone()) .into(), - cosmic::widget::text_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) - .width(Length::Fill) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::text_input("test", &self.entry_value) - .width(Length::Fixed(600.0)) - .padding(32) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::search_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) - .width(Length::Fill) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::text_input("test", &self.entry_value) - .width(Length::Fixed(600.0)) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::search_input("test", &self.entry_value) - .width(Length::Fixed(100.0)) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::search_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) - .padding([24, 48]) - .width(Length::Fixed(400.0)) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::search_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) - .width(Length::Fixed(400.0)) - .on_input(Message::InputChanged) - .into(), - cosmic::widget::search_input("test", &self.entry_value) - .on_clear(Message::InputChanged("".to_string())) - .width(Length::Fixed(800.0)) - .on_input(Message::InputChanged) - .into(), self.color_picker_model .picker_button(Message::ColorPickerUpdate, None) .width(Length::Fixed(128.0)) diff --git a/iced b/iced index d67f1a1c..33b2fd96 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d67f1a1c79cf3dcb378520ef7e4e9ab653f75bea +Subproject commit 33b2fd967ada2d2c86eb1b57eb4997719774499e diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index f2a2bc79..412da483 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -25,6 +25,7 @@ where renderer: &crate::Renderer, bounds: Size, position: Point, + _translation: iced::Vector, ) -> layout::Node { let limits = layout::Limits::new(Size::ZERO, bounds) .width(self.width) diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 5c5f490b..192a76e7 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -192,6 +192,7 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M renderer: &crate::Renderer, bounds: Size, position: Point, + _translation: iced::Vector, ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -514,6 +515,7 @@ impl<'a, S: AsRef, Message> Widget for List<'a, S }, bounds.position(), color, + *viewport, ); } } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index aac00107..cf5f5280 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -195,6 +195,7 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M renderer: &crate::Renderer, bounds: Size, position: Point, + _translation: iced::Vector, ) -> layout::Node { let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -603,6 +604,7 @@ where }, bounds.position(), color, + *viewport, ); } @@ -651,6 +653,7 @@ where }, bounds.position(), appearance.description_color, + *viewport, ); } } diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 3f59fcc2..8b00f4dd 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -147,7 +147,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> _style: &iced_core::renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let font = self .font @@ -168,6 +168,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> .as_ref() .and_then(|id| self.selections.get(id)), tree.state.downcast_ref::>(), + viewport, ); } @@ -486,6 +487,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( font: crate::font::Font, selected: Option<&'a S>, state: &'a State, + viewport: &Rectangle, ) where S: AsRef + 'a, { @@ -547,6 +549,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( }, bounds.position(), style.text_color, + *viewport, ); } } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 37306098..11fbbeb4 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -192,7 +192,7 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo _style: &iced_core::renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { let font = self .font @@ -209,6 +209,7 @@ impl<'a, S: AsRef, Message: 'a> Widget for Dropdo font, self.selected.and_then(|id| self.selections.get(id)), tree.state.downcast_ref::(), + viewport, ); } @@ -479,6 +480,7 @@ pub fn draw<'a, S>( font: crate::font::Font, selected: Option<&'a S>, state: &'a State, + viewport: &Rectangle, ) where S: AsRef + 'a, { @@ -538,6 +540,7 @@ pub fn draw<'a, S>( }, bounds.position(), style.text_color, + *viewport, ); } } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 4c748f30..68dfd35b 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -466,7 +466,13 @@ where Renderer: renderer::Renderer, Renderer::Theme: StyleSheet, { - fn layout(&mut self, renderer: &Renderer, bounds: Size, position: Point) -> Node { + fn layout( + &mut self, + renderer: &Renderer, + bounds: Size, + position: Point, + _translation: iced::Vector, + ) -> Node { // layout children let state = self.tree.state.downcast_mut::(); let overlay_offset = Point::ORIGIN - position; diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 41a6e5c9..9617395d 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -209,7 +209,13 @@ impl<'a, 'b, Message, Renderer> overlay::Overlay where Renderer: iced_core::Renderer, { - fn layout(&mut self, renderer: &Renderer, bounds: Size, mut position: Point) -> layout::Node { + fn layout( + &mut self, + renderer: &Renderer, + bounds: Size, + mut position: Point, + _translation: iced::Vector, + ) -> layout::Node { let limits = layout::Limits::new(Size::UNIT, bounds); let mut node = self .content diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index d2847c43..e6197505 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -644,6 +644,7 @@ where }, bounds.position(), status_appearance.text_color, + *viewport, ); } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 31468188..02386a03 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1904,6 +1904,7 @@ pub fn draw<'a, Message>( }, bounds.position(), appearance.label_color, + *viewport, ); } let mut child_index = 0; @@ -2044,6 +2045,7 @@ pub fn draw<'a, Message>( }, bounds.position(), color, + *viewport, ); }; @@ -2091,6 +2093,7 @@ pub fn draw<'a, Message>( }, helper_text_layout.bounds().position(), appearance.text_color, + *viewport, ); } } From cda781cb96875c1c3b9cf648393ae4f325a1d34d Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:01:50 +0100 Subject: [PATCH 0010/1050] Update mod.rs --- src/app/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index f775aba1..5a487fe8 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -362,7 +362,7 @@ where type Executor: iced_futures::Executor; /// Argument received [`Application::new`]. - type Flags: Clone; + type Flags; /// Message type specific to our app. type Message: Clone + std::fmt::Debug + Send + 'static; From 283aa2abd06d83abf0b8f9c1e38259599d05516a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 5 Dec 2023 09:49:50 -0500 Subject: [PATCH 0011/1050] fix: rectangle tracker missing methods --- src/widget/rectangle_tracker/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 41563a95..801b8fb1 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -93,6 +93,10 @@ where } } + pub fn diff(&mut self, tree: &mut Tree) { + self.container.diff(tree); + } + /// Sets the [`Padding`] of the [`Container`]. #[must_use] pub fn padding>(mut self, padding: P) -> Self { @@ -183,6 +187,10 @@ where self.container.children() } + fn state(&self) -> iced_core::widget::tree::State { + self.container.state() + } + fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } From 6181d96f00781c0616cb73872ecc8c6eca1f7b3c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 5 Dec 2023 12:57:54 +0100 Subject: [PATCH 0012/1050] fix(dropdown::multi::menu): panic in layout --- src/widget/dropdown/multi/menu.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index cf5f5280..83297c8f 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -213,9 +213,7 @@ impl<'a, Message> iced_core::Overlay for Overlay<'a, M ) .width(self.width); - let mut node = self - .container - .layout(&mut self.state.children[0], renderer, &limits); + let mut node = self.container.layout(self.state, renderer, &limits); node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) From 50a94d590af51476dfa4928b595edfa0332d77f2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 5 Dec 2023 15:04:41 +0100 Subject: [PATCH 0013/1050] fix(dropdown::multi::menu): option description off center by 8 pixels --- src/widget/dropdown/multi/menu.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 83297c8f..5cb5d01e 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -584,9 +584,10 @@ where let bounds = Rectangle { x: bounds.x + self.padding.left, - y: bounds.y + self.padding.top, + // TODO: Figure out why it's offset by 8 pixels + y: bounds.y + self.padding.top + 8.0, width: bounds.width, - height: bounds.height, + height: elem_height, }; text::Renderer::fill_text( renderer, From 93cee0abab8cd1a2969b668d2664951af9cfe75f Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 5 Dec 2023 15:58:20 +0100 Subject: [PATCH 0014/1050] fix(dropdown::multi::menu): vertically center-align separators --- src/widget/dropdown/multi/menu.rs | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 5cb5d01e..0faedfea 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -515,7 +515,7 @@ where let mut current_offset = 0.0; for (elem, elem_height) in visible_options { - let bounds = Rectangle { + let mut bounds = Rectangle { x: bounds.x, y: bounds.y + current_offset, width: bounds.width, @@ -530,13 +530,15 @@ where let item_x = bounds.x + appearance.border_width; let item_width = bounds.width - appearance.border_width * 2.0; + bounds = Rectangle { + x: item_x, + width: item_width, + ..bounds + }; + renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: item_x, - width: item_width, - ..bounds - }, + bounds, border_color: Color::TRANSPARENT, border_width: 0.0, border_radius: appearance.border_radius, @@ -563,13 +565,15 @@ where let item_x = bounds.x + appearance.border_width; let item_width = bounds.width - appearance.border_width * 2.0; + bounds = Rectangle { + x: item_x, + width: item_width, + ..bounds + }; + renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: item_x, - width: item_width, - ..bounds - }, + bounds, border_color: Color::TRANSPARENT, border_width: 0.0, border_radius: appearance.border_radius, @@ -617,7 +621,7 @@ where layout_node.move_to(Point { x: bounds.x, - y: bounds.y + self.padding.top, + y: bounds.y + (self.padding.vertical() / 2.0) - 4.0, }); Widget::::draw( From 2cffbaf3a7eda9ff02e55c7bb1c0e806025b620d Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 5 Dec 2023 16:17:06 +0100 Subject: [PATCH 0015/1050] fix(dropdown::multi): set correct line height for descriptions --- src/widget/dropdown/multi/widget.rs | 49 ++++++++++++----------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 8b00f4dd..3989fe8d 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -382,6 +382,10 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static on_selected: &'a dyn Fn(Item) -> Message, ) -> Option> { if state.is_open { + let description_line_height = text::LineHeight::Absolute(Pixels( + text_line_height.to_absolute(Pixels(text_size)).0 + 4.0, + )); + let bounds = layout.bounds(); let menu = Menu::new( @@ -397,33 +401,20 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static None, ) .width({ - let measure = |label: &str, paragraph: &mut crate::Paragraph| { - paragraph.update(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(|| text::Renderer::default_font(renderer)), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - }); - paragraph.min_width().round() - }; - - let measure_description = |label: &str, paragraph: &mut crate::Paragraph| { - paragraph.update(Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size + 4.0), - line_height: text_line_height, - font: font.unwrap_or_else(|| text::Renderer::default_font(renderer)), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - }); - paragraph.min_width().round() - }; + let measure = + |label: &str, paragraph: &mut crate::Paragraph, line_height: text::LineHeight| { + paragraph.update(Text { + content: label, + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height, + font: font.unwrap_or_else(|| text::Renderer::default_font(renderer)), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + }); + paragraph.min_width().round() + }; let mut desc_count = 0; selections @@ -437,7 +428,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static state.descriptions.last_mut().unwrap() }; desc_count += 1; - measure_description(desc.as_ref(), paragraph) + measure(desc.as_ref(), paragraph, description_line_height) } super::menu::OptionElement::Option((option, item)) => { @@ -451,7 +442,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static .push((item.clone(), crate::Paragraph::new())); &mut state.selections.last_mut().unwrap().1 }; - measure(option.as_ref(), paragraph) + measure(option.as_ref(), paragraph, text_line_height) } super::menu::OptionElement::Separator => 1.0, From 360867535841a7127fbf0aa84302eb7dc9a4cc47 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 5 Dec 2023 16:53:20 +0100 Subject: [PATCH 0016/1050] fix(dropdown::multi): paragraphs missing in layout on first init --- src/widget/dropdown/multi/widget.rs | 39 ++++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 3989fe8d..edf1ba29 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -94,14 +94,25 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> self.text_line_height, self.font, self.selections.selected.as_ref().and_then(|id| { - self.selections.get(id).map(AsRef::as_ref).zip( - tree.state - .downcast_mut::>() + self.selections.get(id).map(AsRef::as_ref).zip({ + let state = tree.state.downcast_mut::>(); + + if state.selections.is_empty() { + for list in &self.selections.lists { + for (_, item) in &list.options { + state + .selections + .push((item.clone(), crate::Paragraph::new())); + } + } + } + + state .selections .iter_mut() .find(|(i, _)| i == id) - .map(|(_, p)| p), - ) + .map(|(_, p)| p) + }) }), ) } @@ -432,16 +443,14 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static } super::menu::OptionElement::Option((option, item)) => { - let paragraph = if let Some(index) = - state.selections.iter().position(|(i, _)| i == item) - { - &mut state.selections[index].1 - } else { - state - .selections - .push((item.clone(), crate::Paragraph::new())); - &mut state.selections.last_mut().unwrap().1 - }; + let selection_index = state + .selections + .iter() + .position(|(i, _)| i == item) + .expect("selection missing from state"); + + let paragraph = &mut state.selections[selection_index].1; + measure(option.as_ref(), paragraph, text_line_height) } From fb4669591aacf5677566cb3e7a470e23089a1dca Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 6 Dec 2023 08:05:58 -0700 Subject: [PATCH 0017/1050] Menu: align tree items with menu roots in MenuBar::layout --- src/widget/menu/flex.rs | 2 +- src/widget/menu/menu_bar.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 17c7e104..e9560dbf 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -55,7 +55,7 @@ pub fn resolve<'a, E, Message, Renderer>( spacing: f32, align_items: Alignment, items: &[E], - tree: &mut [Tree], + tree: &mut [&mut Tree], ) -> Node where E: std::borrow::Borrow>, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index a2bcdd3d..3b1be078 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -288,6 +288,12 @@ where .iter() .map(|root| &root.item) .collect::>(); + // the first children of the tree are the menu roots items + let mut tree_children = tree + .children + .iter_mut() + .map(|t| &mut t.children[0]) + .collect::>(); flex::resolve( &flex::Axis::Horizontal, renderer, @@ -296,8 +302,7 @@ where self.spacing, Alignment::Center, &children, - // the children of the tree are the menu roots - &mut tree.children, + &mut tree_children, ) } From 6a8447d70f82ffcf84f27403aafd0c1b8b3ba48c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 6 Dec 2023 08:06:42 -0700 Subject: [PATCH 0018/1050] Menu: unsafe hack to diff children of flattened trees --- src/widget/menu/menu_bar.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 3b1be078..00f377d8 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -216,20 +216,24 @@ where tree.children.truncate(self.menu_roots.len()); } - /*TODO tree.children .iter_mut() .zip(self.menu_roots.iter()) .for_each(|(t, root)| { - let flat = root + let mut flat = root .flattern() .iter() - .map(|mt| mt.item.as_widget()) + .map(|mt| { + let widget = mt.item.as_widget(); + let widget_ptr = widget as *const dyn Widget; + let widget_ptr_mut = widget_ptr as *mut dyn Widget; + //TODO: find a way to diff_children without unsafe code + unsafe { &mut *widget_ptr_mut } + }) .collect::>(); - t.diff_children(&flat); + t.diff_children(flat.as_mut_slice()); }); - */ if tree.children.len() < self.menu_roots.len() { let extended = self.menu_roots[tree.children.len()..].iter().map(|root| { From c2aae2e79ba954732fe0c191df4253b0baf400c3 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 6 Dec 2023 08:07:41 -0700 Subject: [PATCH 0019/1050] Menu: align tree in MenuState::layout --- src/widget/menu/menu_inner.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 68dfd35b..1d782459 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -328,8 +328,7 @@ impl MenuState { .iter() .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) .zip(menu_tree.children[start_index..=end_index].iter()) - .zip(tree[start_index..=end_index].iter_mut()) - .map(|(((cp, size), mt), tree)| { + .map(|((cp, size), mt)| { let mut position = *cp; let mut size = *size; @@ -343,7 +342,10 @@ impl MenuState { let limits = Limits::new(Size::ZERO, size); - let mut node = mt.item.as_widget().layout(tree, renderer, &limits); + let mut node = mt + .item + .as_widget() + .layout(&mut tree[mt.index], renderer, &limits); node.move_to(Point::new(0.0, position + self.scroll_offset)); node }) @@ -493,7 +495,7 @@ where slice, renderer, menu_root, - &mut active_tree.children[start_index..=end_index], + &mut active_tree.children, ); nodes.push(children_node); // only the last menu can have a None active index From 4b8fb4646b1515814f1de4da3e0abd2402b426c8 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 6 Dec 2023 08:15:02 -0700 Subject: [PATCH 0020/1050] Menu: align tree in MenuBounds --- src/widget/menu/menu_inner.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 1d782459..a0f3439c 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -248,17 +248,13 @@ impl MenuBounds { aod: &Aod, bounds_expand: u16, parent_bounds: Rectangle, + tree: &mut [Tree], ) -> Self where Renderer: renderer::Renderer, { - let (children_size, child_positions, child_sizes) = get_children_layout( - menu_tree, - renderer, - item_width, - item_height, - &mut Tree::new(&menu_tree.item), - ); + let (children_size, child_positions, child_sizes) = + get_children_layout(menu_tree, renderer, item_width, item_height, tree); // viewport space parent bounds let view_parent_bounds = parent_bounds + overlay_offset; @@ -809,6 +805,7 @@ fn init_root_menu( &aod, menu.bounds_expand, root_bounds, + &mut menu.tree.children[i].children, ); state.active_root = Some(i); @@ -1072,6 +1069,7 @@ where &aod, menu.bounds_expand, item_bounds, + &mut menu.tree.children[active_root].children, ), }); } @@ -1163,7 +1161,7 @@ fn get_children_layout( renderer: &Renderer, item_width: ItemWidth, item_height: ItemHeight, - tree: &mut Tree, + tree: &mut [Tree], ) -> (Size, Vec, Vec) where Renderer: renderer::Renderer, @@ -1186,15 +1184,14 @@ where ItemHeight::Dynamic(d) => menu_tree .children .iter() - .zip(tree.children.iter_mut()) - .map(|(mt, tree)| { + .map(|mt| { let w = mt.item.as_widget(); match w.height() { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w .layout( - tree, + &mut tree[mt.index], renderer, &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), ) From 77e9a160c4c8e2d04e48970598e256691f991171 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 6 Dec 2023 14:01:28 -0700 Subject: [PATCH 0021/1050] Fix incorrect tree item in ContextDrawer overlay --- src/widget/context_drawer/overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 412da483..9452acc9 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -34,7 +34,7 @@ where let mut node = self.content .as_widget() - .layout(&mut self.tree.children[0], renderer, &limits); + .layout(&mut self.tree, renderer, &limits); let node_size = node.size(); node.move_to(Point { From c66e4aafd0c79a34ea599da347c58c50d32408c1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 Dec 2023 15:27:52 -0500 Subject: [PATCH 0022/1050] update to support winit multi-window --- Cargo.toml | 3 ++ examples/application/src/main.rs | 3 +- examples/cosmic/src/main.rs | 2 +- examples/cosmic/src/window.rs | 8 ++-- examples/open-dialog/src/main.rs | 7 ++- iced | 2 +- src/app/command.rs | 29 +++++++------ src/app/core.rs | 7 ++- src/app/cosmic.rs | 60 +++++++++++++++----------- src/app/mod.rs | 64 +++++++++++++++++++--------- src/app/settings.rs | 4 +- src/command/mod.rs | 49 ++++++++++----------- src/widget/context_drawer/overlay.rs | 8 ++-- 13 files changed, 149 insertions(+), 97 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aeb86e1a..83948625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,10 @@ wayland = [ "iced/wayland", "iced_sctk", "cctk", + "multi-window", ] +# multi-window support +multi-window = ["iced_runtime/multi-window", "iced/multi-window", "iced_winit?/multi-window"] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index d8abad60..a84eca1c 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -4,6 +4,7 @@ //! Application API example use cosmic::app::{Command, Core, Settings}; +use cosmic::iced_core::Size; use cosmic::widget::nav_bar; use cosmic::{executor, iced, ApplicationExt, Element}; @@ -43,7 +44,7 @@ fn main() -> Result<(), Box> { .default_icon_theme("Pop") .default_text_size(16.0) .scale_factor(1.0) - .size((1024, 768)) + .size(Size::new(1024., 768.)) .theme(cosmic::Theme::dark()); cosmic::app::run::(settings, input)?; diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index f180b100..d864f7e5 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -15,6 +15,6 @@ pub fn main() -> cosmic::iced::Result { env_logger::init_from_env(env); cosmic::icon_theme::set_default("Pop"); let mut settings = Settings::default(); - settings.window.min_size = Some((600, 300)); + settings.window.min_size = Some(cosmic::iced::Size::new(600., 300.)); Window::run(settings) } diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index c8e67484..8f621c19 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -436,10 +436,10 @@ impl Application for Window { Message::ToggleNavBarCondensed => { self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed } - Message::Drag => return drag(), - Message::Close => return close(), - Message::Minimize => return minimize(true), - Message::Maximize => return toggle_maximize(), + Message::Drag => return drag(window::Id::MAIN), + Message::Close => return close(window::Id::MAIN), + Message::Minimize => return minimize(window::Id::MAIN, true), + Message::Maximize => return toggle_maximize(window::Id::MAIN), Message::InputChanged => {} diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index a9901413..f14379fc 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -16,7 +16,7 @@ use url::Url; #[rustfmt::skip] fn main() -> Result<(), Box> { let settings = Settings::default() - .size((1024, 768)); + .size(cosmic::iced::Size::new(1024.0, 768.0)); cosmic::app::run::(settings, ())?; @@ -77,7 +77,10 @@ impl cosmic::Application for App { }; app.set_header_title("Open a file".into()); - let cmd = app.set_window_title("COSMIC OpenDialog Demo".into()); + let cmd = app.set_window_title( + "COSMIC OpenDialog Demo".into(), + cosmic::iced::window::Id::MAIN, + ); (app, cmd) } diff --git a/iced b/iced index 33b2fd96..4521d6e5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 33b2fd967ada2d2c86eb1b57eb4997719774499e +Subproject commit 4521d6e584b1cddd00d8d1e7c20784cfd5bcdea9 diff --git a/src/app/command.rs b/src/app/command.rs index 74554c9c..788708dc 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use iced::window; + /// Asynchronous actions for COSMIC applications. use super::Message; @@ -27,16 +29,16 @@ pub mod message { } } -pub fn drag() -> iced::Command> { - crate::command::drag().map(Message::Cosmic) +pub fn drag(id: Option) -> iced::Command> { + crate::command::drag(id).map(Message::Cosmic) } -pub fn fullscreen() -> iced::Command> { - crate::command::fullscreen().map(Message::Cosmic) +pub fn fullscreen(id: Option) -> iced::Command> { + crate::command::fullscreen(id).map(Message::Cosmic) } -pub fn minimize() -> iced::Command> { - crate::command::minimize().map(Message::Cosmic) +pub fn minimize(id: Option) -> iced::Command> { + crate::command::minimize(id).map(Message::Cosmic) } pub fn set_scaling_factor(factor: f32) -> iced::Command> { @@ -47,14 +49,17 @@ pub fn set_theme(theme: crate::Theme) -> iced::Command(title: String) -> iced::Command> { - crate::command::set_title(title).map(Message::Cosmic) +pub fn set_title( + id: Option, + title: String, +) -> iced::Command> { + crate::command::set_title(id, title).map(Message::Cosmic) } -pub fn set_windowed() -> iced::Command> { - crate::command::set_windowed().map(Message::Cosmic) +pub fn set_windowed(id: Option) -> iced::Command> { + crate::command::set_windowed(id).map(Message::Cosmic) } -pub fn toggle_fullscreen() -> iced::Command> { - crate::command::toggle_fullscreen().map(Message::Cosmic) +pub fn toggle_fullscreen(id: Option) -> iced::Command> { + crate::command::toggle_fullscreen(id).map(Message::Cosmic) } diff --git a/src/app/core.rs b/src/app/core.rs index d401f014..6f7ee5e5 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -1,8 +1,11 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use std::collections::HashMap; + use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; +use iced_core::window::Id; use crate::Theme; @@ -56,7 +59,7 @@ pub struct Core { /// Theme mode pub(super) system_theme_mode: ThemeMode, - pub(super) title: String, + pub(super) title: HashMap, pub window: Window, @@ -78,7 +81,7 @@ impl Default for Core { toggled_condensed: true, }, scale_factor: 1.0, - title: String::new(), + title: HashMap::new(), theme_sub_counter: 0, system_theme: crate::theme::active(), system_theme_mode: ThemeMode::config() diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 33042725..46f5c240 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -12,7 +12,13 @@ use cosmic_theme::ThemeMode; use iced::event::wayland::{self, WindowEvent}; #[cfg(feature = "wayland")] use iced::event::PlatformSpecific; +#[cfg(all(feature = "winit", feature = "multi-window"))] +use iced::multi_window::Application as IcedApplication; +#[cfg(feature = "wayland")] +use iced::wayland::Application as IcedApplication; use iced::window; +#[cfg(not(feature = "multi-window"))] +use iced::Application as IcedApplication; use iced_futures::event::listen_raw; #[cfg(not(feature = "wayland"))] use iced_runtime::command::Action; @@ -67,7 +73,7 @@ pub(crate) struct Cosmic { pub(crate) should_exit: bool, } -impl iced::Application for Cosmic +impl IcedApplication for Cosmic where T::Message: Send + 'static, { @@ -82,17 +88,16 @@ where (Self::new(model), command) } - #[cfg(feature = "wayland")] - fn close_requested(&self, id: window::Id) -> Self::Message { - self.app - .on_close_requested(id) - .map_or(super::Message::None, super::Message::App) - } - + #[cfg(not(feature = "multi-window"))] fn title(&self) -> String { self.app.title().to_string() } + #[cfg(feature = "multi-window")] + fn title(&self, id: window::Id) -> String { + self.app.title(id).to_string() + } + fn update(&mut self, message: Self::Message) -> iced::Command { match message { super::Message::App(message) => self.app.update(message), @@ -103,13 +108,14 @@ where } } + #[cfg(not(feature = "multi-window"))] fn scale_factor(&self) -> f64 { f64::from(self.app.core().scale_factor()) } - #[cfg(feature = "wayland")] - fn should_exit(&self) -> bool { - self.should_exit || self.app.should_exit() + #[cfg(feature = "multi-window")] + fn scale_factor(&self, id: window::Id) -> f64 { + f64::from(self.app.core().scale_factor()) } fn style(&self) -> ::Style { @@ -191,13 +197,19 @@ where ]) } + #[cfg(not(feature = "multi-window"))] fn theme(&self) -> Self::Theme { crate::theme::active() } - #[cfg(feature = "wayland")] + #[cfg(feature = "multi-window")] + fn theme(&self, id: window::Id) -> Self::Theme { + crate::theme::active() + } + + #[cfg(feature = "multi-window")] fn view(&self, id: window::Id) -> Element { - if id != window::Id(0) { + if id != window::Id::MAIN { return self.app.view_window(id).map(super::Message::App); } @@ -208,7 +220,7 @@ where } } - #[cfg(not(feature = "wayland"))] + #[cfg(not(feature = "multi-window"))] fn view(&self) -> Element { self.app.view_main() } @@ -224,14 +236,14 @@ impl Cosmic { #[cfg(not(feature = "wayland"))] #[allow(clippy::unused_self)] pub fn close(&mut self) -> iced::Command> { - iced::Command::single(Action::Window(WindowAction::Close)) + iced::Command::single(Action::Window(WindowAction::Close(window::Id::MAIN))) } #[allow(clippy::too_many_lines)] fn cosmic_update(&mut self, message: Message) -> iced::Command> { match message { Message::WindowResize(id, width, height) => { - if window::Id(0) == id { + if window::Id::MAIN == id { self.app.core_mut().set_window_width(width); self.app.core_mut().set_window_height(height); } @@ -241,7 +253,7 @@ impl Cosmic { #[cfg(feature = "wayland")] Message::WindowState(id, state) => { - if window::Id(0) == id { + if window::Id::MAIN == id { self.app.core_mut().window.sharp_corners = state.intersects( WindowState::MAXIMIZED | WindowState::FULLSCREEN @@ -256,7 +268,7 @@ impl Cosmic { #[cfg(feature = "wayland")] Message::WmCapabilities(id, capabilities) => { - if window::Id(0) == id { + if window::Id::MAIN == id { self.app.core_mut().window.can_fullscreen = capabilities.contains(WindowManagerCapabilities::FULLSCREEN); self.app.core_mut().window.show_maximize = @@ -281,25 +293,25 @@ impl Cosmic { keyboard_nav::Message::Escape => return self.app.on_escape(), keyboard_nav::Message::Search => return self.app.on_search(), - keyboard_nav::Message::Fullscreen => return command::toggle_fullscreen(), + keyboard_nav::Message::Fullscreen => return command::toggle_fullscreen(None), }, Message::ContextDrawer(show) => { self.app.core_mut().window.show_context = show; } - Message::Drag => return command::drag(), + Message::Drag => return command::drag(None), - Message::Minimize => return command::minimize(), + Message::Minimize => return command::minimize(None), Message::Maximize => { if self.app.core().window.sharp_corners { self.app.core_mut().window.sharp_corners = false; - return command::set_windowed(); + return command::set_windowed(None); } self.app.core_mut().window.sharp_corners = true; - return command::fullscreen(); + return command::fullscreen(None); } Message::NavBar(key) => { @@ -370,7 +382,7 @@ impl Cosmic { Message::Activate(_token) => { #[cfg(feature = "wayland")] return iced_sctk::commands::activation::activate( - iced::window::Id::default(), + iced::window::Id::MAIN, #[allow(clippy::used_underscore_binding)] _token, ); diff --git a/src/app/mod.rs b/src/app/mod.rs index 5a487fe8..17d37dbf 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -47,6 +47,9 @@ use crate::theme::THEME; use crate::widget::{context_drawer, nav_bar}; use apply::Apply; use iced::Subscription; +#[cfg(all(feature = "winit", feature = "multi-window"))] +use iced::{multi_window::Application as IcedApplication, window}; +#[cfg(any(not(feature = "winit"), not(feature = "multi-window")))] use iced::{window, Application as IcedApplication}; pub use message::Message; use url::Url; @@ -70,8 +73,8 @@ pub(crate) fn iced_settings( let mut core = Core::default(); core.debug = settings.debug; core.set_scale_factor(settings.scale_factor); - core.set_window_width(settings.size.0); - core.set_window_height(settings.size.1); + core.set_window_width(settings.size.width as u32); + core.set_window_height(settings.size.height as u32); THEME.with(move |t| { let mut cosmic_theme = t.borrow_mut(); @@ -98,7 +101,7 @@ pub(crate) fn iced_settings( autosize: settings.autosize, client_decorations: settings.client_decorations, resizable: settings.resizable, - size: settings.size, + size: (settings.size.width as u32, settings.size.height as u32).into(), size_limits: settings.size_limits, title: None, transparent: settings.transparent, @@ -428,11 +431,6 @@ where /// Called before closing the application. fn on_app_exit(&mut self) {} - #[cfg(feature = "wayland")] - fn should_exit(&self) -> bool { - false - } - /// Called when a window requests to be closed. fn on_close_requested(&self, id: window::Id) -> Option { None @@ -471,7 +469,7 @@ where /// Constructs views for other windows. fn view_window(&self, id: window::Id) -> Element { - panic!("no view for window {}", id.0); + panic!("no view for window {:?}", id); } /// Overrides the default style for applications @@ -499,10 +497,15 @@ pub trait ApplicationExt: Application { /// Minimizes the window. fn minimize(&mut self) -> iced::Command>; - /// Get the title of the main window. + + #[cfg(not(feature = "multi-window"))] fn title(&self) -> &str; + #[cfg(feature = "multi-window")] + /// Get the title of a window. + fn title(&self, id: window::Id) -> &str; + /// Set the context drawer title. fn set_context_title(&mut self, title: String) { self.core_mut().set_context_title(title); @@ -513,39 +516,60 @@ pub trait ApplicationExt: Application { self.core_mut().set_header_title(title); } + #[cfg(not(feature = "multi-window"))] /// Set the title of the main window. fn set_window_title(&mut self, title: String) -> iced::Command>; + #[cfg(feature = "multi-window")] + /// Set the title of a window. + fn set_window_title( + &mut self, + title: String, + id: window::Id, + ) -> iced::Command>; + /// View template for the main window. fn view_main(&self) -> Element>; } impl ApplicationExt for App { fn drag(&mut self) -> iced::Command> { - command::drag() + command::drag(Some(window::Id::MAIN)) } fn fullscreen(&mut self) -> iced::Command> { - command::fullscreen() + command::fullscreen(Some(window::Id::MAIN)) } fn minimize(&mut self) -> iced::Command> { - command::minimize() + command::minimize(Some(window::Id::MAIN)) } + #[cfg(feature = "multi-window")] + fn title(&self, id: window::Id) -> &str { + self.core().title.get(&id).map(|s| s.as_str()).unwrap_or("") + } + + #[cfg(not(feature = "multi-window"))] fn title(&self) -> &str { - &self.core().title + &self.core().window.header_title } - #[cfg(feature = "wayland")] - fn set_window_title(&mut self, title: String) -> iced::Command> { - self.core_mut().title = title.clone(); - command::set_title(title) + #[cfg(feature = "multi-window")] + fn set_window_title( + &mut self, + title: String, + id: window::Id, + ) -> iced::Command> { + self.core_mut().title.insert(id, title.clone()); + command::set_title(Some(id), title) } - #[cfg(not(feature = "wayland"))] + #[cfg(not(feature = "multi-window"))] fn set_window_title(&mut self, title: String) -> iced::Command> { - self.core_mut().title = title.clone(); + self.core_mut() + .title + .insert(window::Id::MAIN, title.clone()); iced::Command::none() } diff --git a/src/app/settings.rs b/src/app/settings.rs index 21d649f7..c54b52d4 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -48,7 +48,7 @@ pub struct Settings { pub(crate) scale_factor: f32, /// Initial size of the window. - pub(crate) size: (u32, u32), + pub(crate) size: iced::Size, /// Limitations of the window size #[cfg(feature = "wayland")] @@ -91,7 +91,7 @@ impl Default for Settings { .ok() .and_then(|scale| scale.parse::().ok()) .unwrap_or(1.0), - size: (1024, 768), + size: iced::Size::new(1024.0, 768.0), #[cfg(feature = "wayland")] size_limits: Limits::NONE.min_height(1.0).min_width(1.0), theme: crate::theme::system_preference(), diff --git a/src/command/mod.rs b/src/command/mod.rs index 032e5323..3a918a30 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -3,7 +3,6 @@ //! Create asynchronous actions to be performed in the background. -#[cfg(feature = "wayland")] use iced::window; use iced::Command; use iced_core::window::Mode; @@ -33,45 +32,45 @@ pub fn message(message: M) -> Command { /// Initiates a window drag. #[cfg(feature = "wayland")] -pub fn drag() -> Command { - iced_sctk::commands::window::start_drag_window(window::Id(0)) +pub fn drag(id: Option) -> Command { + iced_sctk::commands::window::start_drag_window(id.unwrap_or(window::Id::MAIN)) } /// Initiates a window drag. #[cfg(not(feature = "wayland"))] -pub fn drag() -> Command { - iced_runtime::window::drag() +pub fn drag(id: Option) -> Command { + iced_runtime::window::drag(id.unwrap_or(window::Id::MAIN)) } /// Fullscreens the window. #[cfg(feature = "wayland")] -pub fn fullscreen() -> Command { - iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Fullscreen) +pub fn fullscreen(id: Option) -> Command { + iced_sctk::commands::window::set_mode_window(id.unwrap_or(window::Id::MAIN), Mode::Fullscreen) } /// Fullscreens the window. #[cfg(not(feature = "wayland"))] -pub fn fullscreen() -> Command { - iced_runtime::window::change_mode(Mode::Fullscreen) +pub fn fullscreen(id: Option) -> Command { + iced_runtime::window::change_mode(id.unwrap_or(window::Id::MAIN), Mode::Fullscreen) } /// Minimizes the window. #[cfg(feature = "wayland")] -pub fn minimize() -> Command { - iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Hidden) +pub fn minimize(id: Option) -> Command { + iced_sctk::commands::window::set_mode_window(id.unwrap_or(window::Id::MAIN), Mode::Hidden) } /// Minimizes the window. #[cfg(not(feature = "wayland"))] -pub fn minimize() -> Command { - iced_runtime::window::minimize(true) +pub fn minimize(id: Option) -> Command { + iced_runtime::window::minimize(id.unwrap_or(window::Id::MAIN), true) } /// Sets the title of a window. #[cfg(feature = "wayland")] -pub fn set_title(title: String) -> Command { +pub fn set_title(id: Option, title: String) -> Command { window_action(WindowAction::Title { - id: window::Id(0), + id: id.unwrap_or(window::Id::MAIN), title, }) } @@ -79,32 +78,34 @@ pub fn set_title(title: String) -> Command { /// Sets the title of a window. #[cfg(not(feature = "wayland"))] #[allow(unused_variables, clippy::needless_pass_by_value)] -pub fn set_title(title: String) -> Command { +pub fn set_title(id: Option, title: String) -> Command { Command::none() } /// Sets the window mode to windowed. #[cfg(feature = "wayland")] -pub fn set_windowed() -> Command { - iced_sctk::commands::window::set_mode_window(window::Id(0), Mode::Windowed) +pub fn set_windowed(id: Option) -> Command { + iced_sctk::commands::window::set_mode_window(id.unwrap_or(window::Id::MAIN), Mode::Windowed) } /// Sets the window mode to windowed. #[cfg(not(feature = "wayland"))] -pub fn set_windowed() -> Command { - iced_runtime::window::change_mode(Mode::Windowed) +pub fn set_windowed(id: Option) -> Command { + iced_runtime::window::change_mode(id.unwrap_or(window::Id::MAIN), Mode::Windowed) } /// Toggles the windows' maximization state. #[cfg(feature = "wayland")] -pub fn toggle_fullscreen() -> Command { - window_action(WindowAction::ToggleFullscreen { id: window::Id(0) }) +pub fn toggle_fullscreen(id: Option) -> Command { + window_action(WindowAction::ToggleFullscreen { + id: id.unwrap_or(window::Id::MAIN), + }) } /// Toggles the windows' maximization state. #[cfg(not(feature = "wayland"))] -pub fn toggle_fullscreen() -> Command { - iced_runtime::window::toggle_maximize() +pub fn toggle_fullscreen(id: Option) -> Command { + iced_runtime::window::toggle_maximize(id.unwrap_or(window::Id::MAIN)) } /// Creates a command to apply an action to a window. diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 9452acc9..a66f3900 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -31,10 +31,10 @@ where .width(self.width) .height(bounds.height - 8.0 - position.y); - let mut node = - self.content - .as_widget() - .layout(&mut self.tree, renderer, &limits); + let mut node = self + .content + .as_widget() + .layout(&mut self.tree, renderer, &limits); let node_size = node.size(); node.move_to(Point { From 685a0543cdf9f1aed360736b9d0601dbcdef29c5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 Dec 2023 16:46:33 -0500 Subject: [PATCH 0023/1050] chore: multi-window example --- examples/multi-window/Cargo.toml | 9 ++ examples/multi-window/src/main.rs | 9 ++ examples/multi-window/src/window.rs | 159 ++++++++++++++++++++++++++++ src/app/cosmic.rs | 4 +- 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 examples/multi-window/Cargo.toml create mode 100644 examples/multi-window/src/main.rs create mode 100644 examples/multi-window/src/window.rs diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml new file mode 100644 index 00000000..97177ce3 --- /dev/null +++ b/examples/multi-window/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "multi-window" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window"] } diff --git a/examples/multi-window/src/main.rs b/examples/multi-window/src/main.rs new file mode 100644 index 00000000..0a5fc03f --- /dev/null +++ b/examples/multi-window/src/main.rs @@ -0,0 +1,9 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod window; +pub use window::*; + +pub fn main() -> cosmic::iced::Result { + cosmic::app::run::(Default::default(), ()) +} diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs new file mode 100644 index 00000000..54052a15 --- /dev/null +++ b/examples/multi-window/src/window.rs @@ -0,0 +1,159 @@ +use std::collections::HashMap; + +use cosmic::{ + app::Core, + iced::{self, event, window}, + iced_core::{id, Alignment, Length, Point}, + iced_widget::{column, scrollable, text, text_input}, + widget::{button, container}, + Command, +}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Message { + CloseWindow(window::Id), + WindowOpened(window::Id, Option), + WindowClosed(window::Id), + NewWindow, + Input(id::Id, String), +} +pub struct MultiWindow { + core: Core, + windows: HashMap, +} + +pub struct Window { + input_id: id::Id, + input_value: String, +} + +impl cosmic::Application for MultiWindow { + type Executor = cosmic::executor::Default; + type Flags = (); + type Message = Message; + + const APP_ID: &'static str = "org.cosmic.MultiWindowDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Command) { + let windows = MultiWindow { + windows: HashMap::from([( + window::Id::MAIN, + Window { + input_id: id::Id::new("main"), + input_value: String::new(), + }, + )]), + core, + }; + + (windows, cosmic::app::Command::none()) + } + + fn subscription(&self) -> cosmic::iced_futures::Subscription { + event::listen_with(|event, _| { + if let iced::Event::Window(id, window_event) = event { + match window_event { + window::Event::CloseRequested => Some(Message::CloseWindow(id)), + window::Event::Opened { position, .. } => { + Some(Message::WindowOpened(id, position)) + } + window::Event::Closed => Some(Message::WindowClosed(id)), + _ => None, + } + } else { + None + } + }) + } + + fn update( + &mut self, + message: Self::Message, + ) -> iced::Command> { + match message { + Message::CloseWindow(id) => window::close(id), + Message::WindowClosed(id) => { + self.windows.remove(&id); + Command::none() + } + Message::WindowOpened(id, ..) => { + if let Some(window) = self.windows.get(&id) { + text_input::focus(window.input_id.clone()) + } else { + Command::none() + } + } + Message::NewWindow => { + let count = self.windows.len() + 1; + + let (id, spawn_window) = window::spawn(window::Settings { + position: Default::default(), + exit_on_close_request: count % 2 == 0, + ..Default::default() + }); + + self.windows.insert( + id, + Window { + input_id: id::Id::new(format!("window_{}", count)), + input_value: String::new(), + }, + ); + + spawn_window + } + Message::Input(id, value) => { + if let Some(w) = self.windows.get_mut(&window::Id::MAIN) { + if id == w.input_id { + w.input_value = value; + } + } + + Command::none() + } + } + } + + fn view_window(&self, id: window::Id) -> cosmic::prelude::Element { + let w = self.windows.get(&id).unwrap(); + + let input_id = w.input_id.clone(); + let input = text_input("something", &w.input_value) + .on_input(move |msg| Message::Input(input_id.clone(), msg)) + .id(w.input_id.clone()); + + let new_window_button = button(text("New Window")).on_press(Message::NewWindow); + + let content = scrollable( + column![input, new_window_button] + .spacing(50) + .width(Length::Fill) + .align_items(Alignment::Center), + ); + + container(container(content).width(200).center_x()) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y() + .into() + } + + fn style( + &self, + ) -> Option<::Style> { + Some(Default::default()) + } + + fn view(&self) -> cosmic::prelude::Element { + self.view_window(window::Id::MAIN) + } +} diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 46f5c240..065b3fcf 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -114,7 +114,7 @@ where } #[cfg(feature = "multi-window")] - fn scale_factor(&self, id: window::Id) -> f64 { + fn scale_factor(&self, _id: window::Id) -> f64 { f64::from(self.app.core().scale_factor()) } @@ -203,7 +203,7 @@ where } #[cfg(feature = "multi-window")] - fn theme(&self, id: window::Id) -> Self::Theme { + fn theme(&self, _id: window::Id) -> Self::Theme { crate::theme::active() } From 77b8718706f1034b7620912a2c03888616e9793c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 Dec 2023 17:32:29 -0500 Subject: [PATCH 0024/1050] fix: headerbar and multi-window example improvements --- examples/multi-window/src/window.rs | 23 ++++++++--------------- src/app/settings.rs | 2 +- src/theme/style/iced.rs | 2 +- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 54052a15..e08f0762 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use cosmic::{ - app::Core, + app::{command::message::cosmic, Core}, iced::{self, event, window}, iced_core::{id, Alignment, Length, Point}, iced_widget::{column, scrollable, text, text_input}, - widget::{button, container}, + widget::{button, container, cosmic_container}, Command, }; @@ -132,14 +132,13 @@ impl cosmic::Application for MultiWindow { let new_window_button = button(text("New Window")).on_press(Message::NewWindow); - let content = scrollable( - column![input, new_window_button] - .spacing(50) - .width(Length::Fill) - .align_items(Alignment::Center), - ); + let content = column![input, new_window_button] + .spacing(50) + .width(Length::Fill) + .align_items(Alignment::Center); - container(container(content).width(200).center_x()) + cosmic_container::container(container(content).width(200).center_x()) + .style(cosmic::style::Container::Background) .width(Length::Fill) .height(Length::Fill) .center_x() @@ -147,12 +146,6 @@ impl cosmic::Application for MultiWindow { .into() } - fn style( - &self, - ) -> Option<::Style> { - Some(Default::default()) - } - fn view(&self) -> cosmic::prelude::Element { self.view_window(window::Id::MAIN) } diff --git a/src/app/settings.rs b/src/app/settings.rs index c54b52d4..0606216d 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -95,7 +95,7 @@ impl Default for Settings { #[cfg(feature = "wayland")] size_limits: Limits::NONE.min_height(1.0).min_width(1.0), theme: crate::theme::system_preference(), - transparent: false, + transparent: true, exit_on_close: true, } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index ba58c136..a6c306fd 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -392,7 +392,7 @@ impl container::StyleSheet for Theme { icon_color: Some(Color::from(palette.accent.base)), text_color: Some(Color::from(palette.background.on)), background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( - Linear::new(Radians(3.0 * PI / 2.0)) + Linear::new(Radians(PI)) .add_stop(0.0, header_top.into()) .add_stop(1.0, header_bottom.into()), ))), From ba90e52848b88b2c28fb266785273634bdf68c91 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 Dec 2023 17:51:20 -0500 Subject: [PATCH 0025/1050] fix: cosmic_container tag method --- examples/multi-window/src/window.rs | 18 ++++++++++-------- src/widget/cosmic_container.rs | 4 ++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index e08f0762..4c9b1407 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use cosmic::{ - app::{command::message::cosmic, Core}, + app::Core, iced::{self, event, window}, iced_core::{id, Alignment, Length, Point}, - iced_widget::{column, scrollable, text, text_input}, - widget::{button, container, cosmic_container}, + iced_widget::{column, container, scrollable, text, text_input}, + widget::{button, cosmic_container}, Command, }; @@ -132,12 +132,14 @@ impl cosmic::Application for MultiWindow { let new_window_button = button(text("New Window")).on_press(Message::NewWindow); - let content = column![input, new_window_button] - .spacing(50) - .width(Length::Fill) - .align_items(Alignment::Center); + let content = scrollable( + column![input, new_window_button] + .spacing(50) + .width(Length::Fill) + .align_items(Alignment::Center), + ); - cosmic_container::container(container(content).width(200).center_x()) + container(container(content).width(200).center_x()) .style(cosmic::style::Container::Background) .width(Length::Fill) .height(Length::Fill) diff --git a/src/widget/cosmic_container.rs b/src/widget/cosmic_container.rs index 4f9c7b78..58548649 100644 --- a/src/widget/cosmic_container.rs +++ b/src/widget/cosmic_container.rs @@ -140,6 +140,10 @@ where self.container.children() } + fn tag(&self) -> iced_core::widget::tree::Tag { + self.container.tag() + } + fn diff(&mut self, tree: &mut Tree) { self.container.diff(tree); } From 2b9e0c09ee1f307a1b488cbd08a633ed30209ee1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 8 Dec 2023 11:58:33 -0500 Subject: [PATCH 0026/1050] fix: applet updates --- .github/workflows/ci.yml | 1 + iced | 2 +- src/applet/mod.rs | 17 +++++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bd7bf50..c432f745 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: - winit - winit_wgpu - wayland + - applet runs-on: ubuntu-22.04 steps: - name: Checkout sources diff --git a/iced b/iced index 4521d6e5..56b6d657 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 4521d6e584b1cddd00d8d1e7c20784cfd5bcdea9 +Subproject commit 56b6d6571ca4292a3c2293ec05d537e12da972e5 diff --git a/src/applet/mod.rs b/src/applet/mod.rs index e6913bb3..f6de708e 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -89,10 +89,13 @@ impl Context { #[allow(clippy::cast_precision_loss)] pub fn window_settings(&self) -> crate::app::Settings { let (width, height) = self.suggested_size(); - let width = u32::from(width); - let height = u32::from(height); + let width = f32::from(width); + let height = f32::from(height); let mut settings = crate::app::Settings::default() - .size((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)) + .size(iced_core::Size::new( + width + APPLET_PADDING as f32 * 2., + height + APPLET_PADDING as f32 * 2., + )) .size_limits( Limits::NONE .min_height(height as f32 + APPLET_PADDING as f32 * 2.0) @@ -235,6 +238,8 @@ pub fn run(autosize: bool, flags: App::Flags) -> iced::Result crate::icon_theme::set_default(icon_theme); } + let (width, height) = (settings.size.width as u32, settings.size.height as u32); + let mut core = Core::default(); core.window.show_window_menu = false; core.window.show_headerbar = false; @@ -245,8 +250,8 @@ pub fn run(autosize: bool, flags: App::Flags) -> iced::Result core.debug = settings.debug; core.set_scale_factor(settings.scale_factor); - core.set_window_width(settings.size.0); - core.set_window_height(settings.size.1); + core.set_window_width(width); + core.set_window_height(height); THEME.with(move |t| { let mut cosmic_theme = t.borrow_mut(); @@ -268,7 +273,7 @@ pub fn run(autosize: bool, flags: App::Flags) -> iced::Result autosize: settings.autosize, client_decorations: settings.client_decorations, resizable: settings.resizable, - size: settings.size, + size: (width, height), size_limits: settings.size_limits, title: None, transparent: settings.transparent, From 74ee5084273aeb12a21b5b85bd750b51f9462de6 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Sat, 9 Dec 2023 01:15:47 +0100 Subject: [PATCH 0027/1050] fix(dropdown::multi): panic on missing paragraph --- src/widget/dropdown/multi/widget.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index edf1ba29..ea43f643 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -443,11 +443,17 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static } super::menu::OptionElement::Option((option, item)) => { - let selection_index = state - .selections - .iter() - .position(|(i, _)| i == item) - .expect("selection missing from state"); + let selection_index = state.selections.iter().position(|(i, _)| i == item); + + let selection_index = match selection_index { + Some(index) => index, + None => { + state + .selections + .push((item.clone(), crate::Paragraph::new())); + state.selections.len() - 1 + } + }; let paragraph = &mut state.selections[selection_index].1; From 493bf6c47a46e735bbb91a2ee06629839bfca911 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:59:13 -0500 Subject: [PATCH 0028/1050] fix: avoid accidentally triggering vendoring of iced_winit when not used (#238) --- Cargo.toml | 3 +-- iced | 2 +- src/app/cosmic.rs | 18 +++++++++--------- src/app/mod.rs | 16 ++++++++-------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83948625..777c9e4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,9 @@ wayland = [ "iced/wayland", "iced_sctk", "cctk", - "multi-window", ] # multi-window support -multi-window = ["iced_runtime/multi-window", "iced/multi-window", "iced_winit?/multi-window"] +multi-window = ["iced/multi-window"] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit diff --git a/iced b/iced index 56b6d657..8195c7f5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 56b6d6571ca4292a3c2293ec05d537e12da972e5 +Subproject commit 8195c7f50de6f07ea7475861f9be5139d3fa61e3 diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 065b3fcf..24c00035 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -17,7 +17,7 @@ use iced::multi_window::Application as IcedApplication; #[cfg(feature = "wayland")] use iced::wayland::Application as IcedApplication; use iced::window; -#[cfg(not(feature = "multi-window"))] +#[cfg(not(any(feature = "multi-window", feature = "wayland")))] use iced::Application as IcedApplication; use iced_futures::event::listen_raw; #[cfg(not(feature = "wayland"))] @@ -88,12 +88,12 @@ where (Self::new(model), command) } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn title(&self) -> String { self.app.title().to_string() } - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] fn title(&self, id: window::Id) -> String { self.app.title(id).to_string() } @@ -108,12 +108,12 @@ where } } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn scale_factor(&self) -> f64 { f64::from(self.app.core().scale_factor()) } - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] fn scale_factor(&self, _id: window::Id) -> f64 { f64::from(self.app.core().scale_factor()) } @@ -197,17 +197,17 @@ where ]) } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn theme(&self) -> Self::Theme { crate::theme::active() } - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] fn theme(&self, _id: window::Id) -> Self::Theme { crate::theme::active() } - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] fn view(&self, id: window::Id) -> Element { if id != window::Id::MAIN { return self.app.view_window(id).map(super::Message::App); @@ -220,7 +220,7 @@ where } } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn view(&self) -> Element { self.app.view_main() } diff --git a/src/app/mod.rs b/src/app/mod.rs index 17d37dbf..651f0469 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -499,10 +499,10 @@ pub trait ApplicationExt: Application { fn minimize(&mut self) -> iced::Command>; /// Get the title of the main window. - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn title(&self) -> &str; - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] /// Get the title of a window. fn title(&self, id: window::Id) -> &str; @@ -516,11 +516,11 @@ pub trait ApplicationExt: Application { self.core_mut().set_header_title(title); } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] /// Set the title of the main window. fn set_window_title(&mut self, title: String) -> iced::Command>; - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] /// Set the title of a window. fn set_window_title( &mut self, @@ -545,17 +545,17 @@ impl ApplicationExt for App { command::minimize(Some(window::Id::MAIN)) } - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] fn title(&self, id: window::Id) -> &str { self.core().title.get(&id).map(|s| s.as_str()).unwrap_or("") } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn title(&self) -> &str { &self.core().window.header_title } - #[cfg(feature = "multi-window")] + #[cfg(any(feature = "multi-window", feature = "wayland"))] fn set_window_title( &mut self, title: String, @@ -565,7 +565,7 @@ impl ApplicationExt for App { command::set_title(Some(id), title) } - #[cfg(not(feature = "multi-window"))] + #[cfg(not(any(feature = "multi-window", feature = "wayland")))] fn set_window_title(&mut self, title: String) -> iced::Command> { self.core_mut() .title From 83fbde77ab020a4f74f86b9d478a209981912502 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 11 Dec 2023 14:02:17 -0800 Subject: [PATCH 0029/1050] Update dependencies (#239) Testing this I noticed building with the `animated-image` feature is currently failing. But that's unrelated to the changes here. --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 777c9e4c..c404206d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,19 +53,19 @@ derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.7.3" tokio = { version = "1.24.2", optional = true } -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "5faec87", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c1b6516", optional = true } slotmap = "1.0.6" -fraction = "0.13.0" +fraction = "0.14.0" cosmic-config = { path = "cosmic-config" } tracing = "0.1" image = { version = "0.24.6", optional = true } thiserror = "1.0.44" -async-fs = { version = "1.6", optional = true } -ashpd = { version = "0.5.0", default-features = false, optional = true } +async-fs = { version = "2.1", optional = true } +ashpd = { version = "0.6.0", default-features = false, optional = true } url = "2.4.0" unicode-segmentation = "1.6" css-color = "0.2.5" -nix = { version = "0.26", optional = true } +nix = { version = "0.27", features = ["process"], optional = true } zbus = {version = "3.14.1", default-features = false, optional = true} serde = { version = "1.0.180", optional = true } From 56965ac2e5841252fca4e628b3dc0db52094faa2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 12 Dec 2023 15:01:51 +0100 Subject: [PATCH 0030/1050] fix(app): closing of window in wayland --- iced | 2 +- src/app/cosmic.rs | 11 ++--------- src/app/mod.rs | 2 +- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/iced b/iced index 8195c7f5..55759e5b 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 8195c7f50de6f07ea7475861f9be5139d3fa61e3 +Subproject commit 55759e5bb79f84277ccf198f529af04184912874 diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 24c00035..2b405f5f 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -69,8 +69,6 @@ pub enum Message { #[derive(Default)] pub(crate) struct Cosmic { pub(crate) app: App, - #[cfg(feature = "wayland")] - pub(crate) should_exit: bool, } impl IcedApplication for Cosmic @@ -229,8 +227,7 @@ where impl Cosmic { #[cfg(feature = "wayland")] pub fn close(&mut self) -> iced::Command> { - self.should_exit = true; - iced::Command::none() + iced_sctk::commands::window::close_window(window::Id::MAIN) } #[cfg(not(feature = "wayland"))] @@ -395,10 +392,6 @@ impl Cosmic { impl Cosmic { pub fn new(app: App) -> Self { - Self { - app, - #[cfg(feature = "wayland")] - should_exit: false, - } + Self { app } } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 651f0469..76b89474 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -469,7 +469,7 @@ where /// Constructs views for other windows. fn view_window(&self, id: window::Id) -> Element { - panic!("no view for window {:?}", id); + panic!("no view for window {id:?}"); } /// Overrides the default style for applications From 310064ca1dccf86e89a166c350d7bd212ef23052 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 13 Dec 2023 12:59:26 -0500 Subject: [PATCH 0031/1050] refactor: use theme corner radii everywhere not all values matched a default value but I picked the next closest in that case --- examples/cosmic/Cargo.toml | 2 +- src/applet/mod.rs | 2 +- src/theme/style/dropdown.rs | 2 +- src/theme/style/iced.rs | 52 ++++++++++---------- src/theme/style/segmented_button.rs | 71 ++++++++++++++++----------- src/widget/button/style.rs | 5 +- src/widget/button/widget.rs | 14 +++++- src/widget/context_drawer/widget.rs | 2 +- src/widget/list/mod.rs | 2 +- src/widget/nav_bar.rs | 2 +- src/widget/search/field.rs | 2 +- src/widget/segmented_button/widget.rs | 5 +- src/widget/text_input/input.rs | 7 ++- src/widget/warning.rs | 3 +- 14 files changed, 103 insertions(+), 68 deletions(-) diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index fdc939b9..7e7b466f 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] apply = "0.3.0" -fraction = "0.13.0" +fraction = "0.14.0" libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance"] } once_cell = "1.18" slotmap = "1.0.6" diff --git a/src/applet/mod.rs b/src/applet/mod.rs index f6de708e..dcfd271e 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -157,7 +157,7 @@ impl Context { Appearance { text_color: Some(cosmic.background.on.into()), background: Some(Color::from(cosmic.background.base).into()), - border_radius: 12.0.into(), + border_radius: cosmic.corner_radii.radius_m.into(), border_width: 1.0, border_color: cosmic.background.divider.into(), icon_color: Some(cosmic.background.on.into()), diff --git a/src/theme/style/dropdown.rs b/src/theme/style/dropdown.rs index 17a8b9fe..f62ab984 100644 --- a/src/theme/style/dropdown.rs +++ b/src/theme/style/dropdown.rs @@ -15,7 +15,7 @@ impl dropdown::menu::StyleSheet for Theme { text_color: cosmic.on_bg_color().into(), background: Background::Color(cosmic.background.component.base.into()), border_width: 0.0, - border_radius: 16.0.into(), + border_radius: cosmic.corner_radii.radius_m.into(), border_color: Color::TRANSPARENT, hovered_text_color: cosmic.on_bg_color().into(), diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index a6c306fd..3624c404 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -367,6 +367,7 @@ impl container::StyleSheet for Theme { #[allow(clippy::too_many_lines)] fn appearance(&self, style: &Self::Style) -> container::Appearance { + let cosmic = self.cosmic(); match style { Container::Transparent => container::Appearance::default(), Container::Custom(f) => f(self), @@ -377,7 +378,7 @@ impl container::StyleSheet for Theme { icon_color: Some(Color::from(palette.background.on)), text_color: Some(Color::from(palette.background.on)), background: Some(iced::Background::Color(palette.background.base.into())), - border_radius: 2.0.into(), + border_radius: cosmic.corner_radii.radius_xs.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -413,7 +414,7 @@ impl container::StyleSheet for Theme { icon_color: Some(Color::from(palette.primary.on)), text_color: Some(Color::from(palette.primary.on)), background: Some(iced::Background::Color(palette.primary.base.into())), - border_radius: 2.0.into(), + border_radius: cosmic.corner_radii.radius_xs.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -425,7 +426,7 @@ impl container::StyleSheet for Theme { icon_color: Some(Color::from(palette.secondary.on)), text_color: Some(Color::from(palette.secondary.on)), background: Some(iced::Background::Color(palette.secondary.base.into())), - border_radius: 2.0.into(), + border_radius: cosmic.corner_radii.radius_xs.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -438,7 +439,7 @@ impl container::StyleSheet for Theme { icon_color: None, text_color: None, background: Some(iced::Background::Color(theme.primary.base.into())), - border_radius: f32::from(theme.space_xxs()).into(), + border_radius: theme.corner_radii.radius_xs.into(), border_width: 1.0, border_color: theme.bg_divider().into(), } @@ -451,7 +452,7 @@ impl container::StyleSheet for Theme { icon_color: None, text_color: None, background: Some(iced::Background::Color(theme.palette.neutral_2.into())), - border_radius: f32::from(theme.space_xl()).into(), + border_radius: theme.corner_radii.radius_l.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } @@ -467,7 +468,7 @@ impl container::StyleSheet for Theme { background: Some(iced::Background::Color( palette.background.component.base.into(), )), - border_radius: 8.0.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -477,7 +478,7 @@ impl container::StyleSheet for Theme { background: Some(iced::Background::Color( palette.primary.component.base.into(), )), - border_radius: 8.0.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -487,7 +488,7 @@ impl container::StyleSheet for Theme { background: Some(iced::Background::Color( palette.secondary.component.base.into(), )), - border_radius: 8.0.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -530,7 +531,7 @@ impl slider::StyleSheet for Theme { Color::TRANSPARENT, ), width: 4.0, - border_radius: 2.0.into(), + border_radius: cosmic.corner_radii.radius_xs.into(), }, handle: slider::Handle { @@ -588,7 +589,7 @@ impl menu::StyleSheet for Theme { text_color: cosmic.on_bg_color().into(), background: Background::Color(cosmic.background.base.into()), border_width: 0.0, - border_radius: 16.0.into(), + border_radius: cosmic.corner_radii.radius_m.into(), border_color: Color::TRANSPARENT, selected_text_color: cosmic.accent.base.into(), selected_background: Background::Color(cosmic.background.component.hover.into()), @@ -609,7 +610,7 @@ impl pick_list::StyleSheet for Theme { text_color: cosmic.on_bg_color().into(), background: Color::TRANSPARENT.into(), placeholder_color: cosmic.on_bg_color().into(), - border_radius: 24.0.into(), + border_radius: cosmic.corner_radii.radius_m.into(), border_width: 0.0, border_color: Color::TRANSPARENT, // icon_size: 0.7, // TODO: how to replace @@ -748,7 +749,7 @@ impl pane_grid::StyleSheet for Theme { background: Background::Color(theme.bg_color().into()), border_width: 2.0, border_color: theme.bg_divider().into(), - border_radius: 0.0.into(), + border_radius: theme.corner_radii.radius_0.into(), } } } @@ -781,17 +782,17 @@ impl progress_bar::StyleSheet for Theme { ProgressBar::Primary => progress_bar::Appearance { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.accent.base).into(), - border_radius: 2.0.into(), + border_radius: theme.corner_radii.radius_xs.into(), }, ProgressBar::Success => progress_bar::Appearance { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.success.base).into(), - border_radius: 2.0.into(), + border_radius: theme.corner_radii.radius_xs.into(), }, ProgressBar::Danger => progress_bar::Appearance { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.destructive.base).into(), - border_radius: 2.0.into(), + border_radius: theme.corner_radii.radius_xs.into(), }, ProgressBar::Custom(f) => f(self), } @@ -851,16 +852,17 @@ impl scrollable::StyleSheet for Theme { type Style = (); fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { + let cosmic = self.cosmic(); scrollable::Scrollbar { background: Some(Background::Color( self.current_container().component.base.into(), )), - border_radius: 4.0.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: self.current_container().component.divider.into(), - border_radius: 4.0.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -878,12 +880,12 @@ impl scrollable::StyleSheet for Theme { background: Some(Background::Color( self.current_container().component.hover.into(), )), - border_radius: 4.0.into(), + border_radius: theme.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { color: theme.accent.base.into(), - border_radius: 4.0.into(), + border_radius: theme.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -972,14 +974,14 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 8.0.into(), + border_radius: palette.corner_radii.radius_s.into(), border_width: 1.0, border_color: self.current_container().component.divider.into(), icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 24.0.into(), + border_radius: palette.corner_radii.radius_m.into(), border_width: 0.0, border_color: Color::TRANSPARENT, icon_color: self.current_container().on.into(), @@ -995,14 +997,14 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 8.0.into(), + border_radius: palette.corner_radii.radius_s.into(), border_width: 1.0, border_color: palette.accent.base.into(), icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 24.0.into(), + border_radius: palette.corner_radii.radius_m.into(), border_width: 0.0, border_color: Color::TRANSPARENT, icon_color: self.current_container().on.into(), @@ -1018,14 +1020,14 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 8.0.into(), + border_radius: palette.corner_radii.radius_s.into(), border_width: 1.0, border_color: palette.accent.base.into(), icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), - border_radius: 24.0.into(), + border_radius: palette.corner_radii.radius_m.into(), border_width: 0.0, border_color: Color::TRANSPARENT, icon_color: self.current_container().on.into(), diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 57fc62d5..ef510c60 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -29,21 +29,21 @@ impl StyleSheet for Theme { let cosmic = self.cosmic(); let active = horizontal::view_switcher_active(cosmic); Appearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), inactive: ItemStatusAppearance { background: None, first: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), border_bottom: Some((1.0, cosmic.accent.base.into())), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), border_bottom: Some((1.0, cosmic.accent.base.into())), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), border_bottom: Some((1.0, cosmic.accent.base.into())), ..Default::default() }, @@ -60,20 +60,24 @@ impl StyleSheet for Theme { let active = horizontal::selection_active(cosmic); let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; + let rad_m = cosmic.corner_radii.radius_m; + let rad_0 = cosmic.corner_radii.radius_0; Appearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), inactive: ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + border_radius: BorderRadius::from([rad_m[0], rad_0[1], rad_0[2], 24.0]), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + border_radius: BorderRadius::from([ + rad_0[0], rad_m[1], rad_m[2], rad_0[3], + ]), ..Default::default() }, text_color: cosmic.on_bg_color().into(), @@ -90,12 +94,14 @@ impl StyleSheet for Theme { #[allow(clippy::too_many_lines)] fn vertical(&self, style: &Self::Style) -> Appearance { + let cosmic = self.cosmic(); + let rad_m = cosmic.corner_radii.radius_m; + let rad_0 = cosmic.corner_radii.radius_0; match style { SegmentedButton::ViewSwitcher => { - let cosmic = self.cosmic(); let active = vertical::view_switcher_active(cosmic); Appearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), inactive: ItemStatusAppearance { background: None, text_color: cosmic.on_bg_color().into(), @@ -108,24 +114,27 @@ impl StyleSheet for Theme { } } SegmentedButton::Selection => { - let cosmic = self.cosmic(); let active = vertical::selection_active(cosmic); let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; Appearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), inactive: ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + border_radius: BorderRadius::from([ + rad_m[0], rad_m[1], rad_0[0], rad_0[0], + ]), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + border_radius: BorderRadius::from([ + rad_0[0], rad_0[1], rad_m[2], rad_m[3], + ]), ..Default::default() }, text_color: cosmic.on_bg_color().into(), @@ -144,23 +153,25 @@ impl StyleSheet for Theme { mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; - use palette::{rgb::Rgb, Alpha}; + use palette::{rgb::Rgb, white_point::C, Alpha}; pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; + let rad_m = cosmic.corner_radii.radius_m; + let rad_0 = cosmic.corner_radii.radius_0; ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + border_radius: BorderRadius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + border_radius: BorderRadius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), ..Default::default() }, text_color: cosmic.accent.base.into(), @@ -172,20 +183,22 @@ mod horizontal { ) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; + let rad_s = cosmic.corner_radii.radius_s; + let rad_0 = cosmic.corner_radii.radius_0; ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_radius: BorderRadius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_radius: BorderRadius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_radius: BorderRadius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, @@ -230,18 +243,20 @@ mod vertical { pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; + let rad_0 = cosmic.corner_radii.radius_0; + let rad_m = cosmic.corner_radii.radius_m; ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + border_radius: BorderRadius::from([rad_m[0], rad_m[1], rad_0[2], rad_0[3]]), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from(0.0), + border_radius: cosmic.corner_radii.radius_0.into(), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + border_radius: BorderRadius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), ..Default::default() }, text_color: cosmic.accent.base.into(), @@ -256,15 +271,15 @@ mod vertical { ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from(24.0), + border_radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from(24.0), + border_radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from(24.0), + border_radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, text_color: cosmic.accent.base.into(), diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs index 644aedb6..88a22539 100644 --- a/src/widget/button/style.rs +++ b/src/widget/button/style.rs @@ -4,6 +4,8 @@ //! Change the apperance of a button. use iced_core::{Background, BorderRadius, Color, Vector}; +use crate::theme::THEME; + /// The appearance of a button. #[must_use] #[derive(Debug, Clone, Copy)] @@ -39,10 +41,11 @@ pub struct Appearance { impl Appearance { // TODO: `BorderRadius` is not `const fn` compatible. pub fn new() -> Self { + let rad_0 = THEME.with(|t| t.borrow().cosmic().corner_radii.radius_0); Self { shadow_offset: Vector::new(0.0, 0.0), background: None, - border_radius: BorderRadius::from(0.0), + border_radius: BorderRadius::from(rad_0), border_width: 0.0, border_color: Color::TRANSPARENT, outline_width: 0.0, diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 37cb58f6..7e5e301b 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -23,6 +23,8 @@ use iced_core::{ }; use iced_renderer::core::widget::{operation, OperationOutputWrapper}; +use crate::theme::THEME; + pub use super::style::{Appearance, StyleSheet}; /// Internally defines different button widget variants. @@ -387,6 +389,8 @@ where { let selection_background = theme.selection_background(); + let c_rad = THEME.with(|t| t.borrow().cosmic().corner_radii); + if self.selected { renderer.fill_quad( Quad { @@ -396,7 +400,13 @@ where x: bounds.x + styling.border_width, y: bounds.y + (bounds.height - 20.0 - styling.border_width), }, - border_radius: [0.0, 8.0, 0.0, 8.0].into(), + border_radius: [ + c_rad.radius_0[0], + c_rad.radius_s[1], + c_rad.radius_0[2], + c_rad.radius_s[3], + ] + .into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -423,7 +433,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: 20.0.into(), + border_radius: c_rad.radius_m.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index dee04a86..06ddf76e 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -84,7 +84,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { icon_color: Some(Color::from(palette.primary.on)), text_color: Some(Color::from(palette.primary.on)), background: Some(iced::Background::Color(palette.primary.base.into())), - border_radius: 8.0.into(), + border_radius: palette.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index 0217fe4f..6ad5855b 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -28,7 +28,7 @@ pub fn style(theme: &crate::Theme) -> crate::widget::container::Appearance { icon_color: Some(container.on.into()), text_color: Some(container.on.into()), background: Some(Background::Color(container.base.into())), - border_radius: 8.0.into(), + border_radius: theme.cosmic().corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index a62d3c60..fd888ebe 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -48,7 +48,7 @@ pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance { icon_color: Some(cosmic.on_bg_color().into()), text_color: Some(cosmic.on_bg_color().into()), background: Some(Background::Color(cosmic.primary.base.into())), - border_radius: 8.0.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } diff --git a/src/widget/search/field.rs b/src/widget/search/field.rs index 0ca5af1d..d1176559 100644 --- a/src/widget/search/field.rs +++ b/src/widget/search/field.rs @@ -81,7 +81,7 @@ fn active_style(theme: &crate::Theme) -> container::Appearance { icon_color: Some(cosmic.palette.neutral_9.into()), text_color: Some(cosmic.palette.neutral_9.into()), background: Some(Background::Color(neutral_7.into())), - border_radius: 24.0.into(), + border_radius: cosmic.corner_radii.radius_m.into(), border_width: 2.0, border_color: cosmic.accent.focus.into(), } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index e6197505..90844419 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use super::model::{Entity, Model, Selectable}; -use crate::theme::SegmentedButton as Style; +use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::{icon, Icon}; use crate::{Element, Renderer}; use derive_setters::Setters; @@ -558,10 +558,11 @@ where bounds.y = bounds.y + bounds.height - width; bounds.height = width; + let rad_0 = THEME.with(|t| t.borrow().cosmic().corner_radii.radius_0); renderer.fill_quad( renderer::Quad { bounds, - border_radius: BorderRadius::from(0.0), + border_radius: rad_0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 02386a03..1b756d13 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1933,6 +1933,9 @@ pub fn draw<'a, Message>( let font = font.unwrap_or_else(|| renderer.default_font()); let size = size.unwrap_or_else(|| renderer.default_size().0); + let radius_0 = THEME + .with(|t| t.borrow().cosmic().corner_radii.radius_0) + .into(); let (cursor, offset) = if let Some(focus) = &state.is_focused { match state.cursor.state(value) { cursor::State::Index(position) => { @@ -1956,7 +1959,7 @@ pub fn draw<'a, Message>( width: 1.0, height: text_bounds.height, }, - border_radius: 0.0.into(), + border_radius: radius_0, border_width: 0.0, border_color: Color::TRANSPARENT, }, @@ -1994,7 +1997,7 @@ pub fn draw<'a, Message>( width, height: text_bounds.height, }, - border_radius: 0.0.into(), + border_radius: radius_0, border_width: 0.0, border_color: Color::TRANSPARENT, }, diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 38c364ee..1f0c8147 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -57,11 +57,12 @@ impl<'a, Message: 'static + Clone> From> for Element<'a, Me #[must_use] pub fn warning_container(theme: &Theme) -> widget::container::Appearance { + let cosmic = theme.cosmic(); widget::container::Appearance { icon_color: Some(theme.cosmic().warning.on.into()), text_color: Some(theme.cosmic().warning.on.into()), background: Some(Background::Color(theme.cosmic().warning_color().into())), - border_radius: 0.0.into(), + border_radius: cosmic.corner_radii.radius_0.into(), border_width: 0.0, border_color: Color::TRANSPARENT, } From 696437506c783da5ce588d26d1f2c2f550c846f1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 13 Dec 2023 13:16:44 -0500 Subject: [PATCH 0032/1050] fix: missed one corner radius in segmented buttons --- src/theme/style/segmented_button.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index ef510c60..cd6344d1 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -67,7 +67,9 @@ impl StyleSheet for Theme { inactive: ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([rad_m[0], rad_0[1], rad_0[2], 24.0]), + border_radius: BorderRadius::from([ + rad_m[0], rad_0[1], rad_0[2], rad_m[3], + ]), ..Default::default() }, middle: ItemAppearance { From 8725497827e87536ac9cad5ddad1a537d68fb8f4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 13 Dec 2023 15:08:14 -0500 Subject: [PATCH 0033/1050] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 55759e5b..51723e87 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 55759e5bb79f84277ccf198f529af04184912874 +Subproject commit 51723e87f7de6f9ce8943c28acd0ab62270675f6 From d53f693a3715fdd79481d75652cbf74286f4f387 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Dec 2023 23:38:38 +0100 Subject: [PATCH 0034/1050] fix(flex-row): awkward breakpoint after first row --- src/widget/flex_row/layout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index bcd9e4a9..108c6c18 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -34,7 +34,7 @@ pub fn resolve( let size = child_node.size(); // Calculate the required additional width to fit the item into the current row. - let required_width = size.width + let mut required_width = size.width + if row_buffer.is_empty() { 0.0 } else { @@ -58,7 +58,7 @@ pub fn resolve( flex_height += current_row_height; flex_width = flex_width.max(current_row_width); - + required_width -= row_spacing; current_row_width = 0.0; } From 11428723e55dfebad617cedf2636a1a8c804d42c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 19 Dec 2023 11:09:03 -0700 Subject: [PATCH 0035/1050] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 51723e87..7a260079 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 51723e87f7de6f9ce8943c28acd0ab62270675f6 +Subproject commit 7a2600793c736875d4a9c61148ed741bde631462 From 18a5c670656f53aed4b9318b633f925403ba2e02 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 19 Dec 2023 17:07:35 -0700 Subject: [PATCH 0036/1050] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7a260079..611ce160 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7a2600793c736875d4a9c61148ed741bde631462 +Subproject commit 611ce160e6b2ccab388fc8e303d4795a3365afbc From b8f1a366dd030b90ed72e50f521e3da1d6a676ce Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 21 Dec 2023 11:53:19 -0700 Subject: [PATCH 0037/1050] Add content_container flag to enable/disable wrapping content --- src/app/core.rs | 2 ++ src/app/mod.rs | 74 ++++++++++++++++++++++++++----------------------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/app/core.rs b/src/app/core.rs index 6f7ee5e5..37a5fade 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -26,6 +26,7 @@ pub struct Window { /// Label to display as header bar title. pub header_title: String, pub use_template: bool, + pub content_container: bool, pub can_fullscreen: bool, pub sharp_corners: bool, pub show_context: bool, @@ -98,6 +99,7 @@ impl Default for Core { context_title: String::new(), header_title: String::new(), use_template: true, + content_container: true, can_fullscreen: false, sharp_corners: false, show_context: false, diff --git a/src/app/mod.rs b/src/app/mod.rs index 76b89474..9ef1ed43 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -578,6 +578,45 @@ impl ApplicationExt for App { let core = self.core(); let is_condensed = core.is_condensed(); + let content_row = crate::widget::row::with_children({ + let mut widgets = Vec::with_capacity(2); + + // Insert nav bar onto the left side of the window. + if let Some(nav) = self.nav_bar() { + widgets.push(nav.debug(core.debug)); + } + + if self.nav_model().is_none() || core.show_content() { + let main_content = self.view().debug(core.debug).map(Message::App); + + widgets.push(if let Some(context) = self.context_drawer() { + context_drawer( + &core.window.context_title, + Message::Cosmic(cosmic::Message::ContextDrawer(false)), + main_content, + context.map(Message::App), + ) + .into() + } else { + main_content + }); + } + + widgets + }) + .spacing(8); + let content: Element<_> = if core.window.content_container { + content_row + .apply(crate::widget::container) + .padding([0, 8, 8, 8]) + .width(iced::Length::Fill) + .height(iced::Length::Fill) + .style(crate::theme::Container::Background) + .into() + } else { + content_row.into() + }; + crate::widget::column::with_capacity(2) .push_maybe(if core.window.show_headerbar { Some({ @@ -624,40 +663,7 @@ impl ApplicationExt for App { None }) // The content element contains every element beneath the header. - .push( - crate::widget::row::with_children({ - let mut widgets = Vec::with_capacity(2); - - // Insert nav bar onto the left side of the window. - if let Some(nav) = self.nav_bar() { - widgets.push(nav.debug(core.debug)); - } - - if self.nav_model().is_none() || core.show_content() { - let main_content = self.view().debug(core.debug).map(Message::App); - - widgets.push(if let Some(context) = self.context_drawer() { - context_drawer( - &core.window.context_title, - Message::Cosmic(cosmic::Message::ContextDrawer(false)), - main_content, - context.map(Message::App), - ) - .into() - } else { - main_content - }); - } - - widgets - }) - .spacing(8) - .apply(crate::widget::container) - .padding([0, 8, 8, 8]) - .width(iced::Length::Fill) - .height(iced::Length::Fill) - .style(crate::theme::Container::Background), - ) + .push(content) .into() } } From a731ebc1b2d45fcc504ba5d6ae54a0e2d60da599 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 27 Dec 2023 16:03:09 -0500 Subject: [PATCH 0038/1050] fix: set platform specific app id --- src/app/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 9ef1ed43..4921300c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -88,6 +88,10 @@ pub(crate) fn iced_settings( iced.default_text_size = iced::Pixels(settings.default_text_size); iced.exit_on_close_request = settings.exit_on_close; iced.id = Some(App::APP_ID.to_owned()); + #[cfg(all(not(feature = "wayland"), target_os = "linux"))] + { + iced.window.platform_specific.application_id = App::APP_ID.to_string(); + } #[cfg(feature = "wayland")] { From 983d4d3378766e6f9f9d28ecf9e9c3f0816107ad Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Dec 2023 12:09:32 -0500 Subject: [PATCH 0039/1050] feat: custom icon fallbacks --- src/widget/icon/named.rs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index e802b2ae..63d8e95e 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -2,7 +2,17 @@ // SPDX-License-Identifier: MPL-2.0 use super::{Handle, Icon}; -use std::{path::PathBuf, sync::Arc}; +use std::{borrow::Cow, path::PathBuf, sync::Arc}; + +#[derive(Debug, Clone, Default, Hash)] +/// Fallback icon to use if the icon was not found. +pub enum IconFallback { + #[default] + /// Default fallback using the icon name. + Default, + /// Fallback to specific icon names. + Names(Vec>), +} #[must_use] #[derive(derive_setters::Setters, Clone, Debug, Hash)] @@ -11,7 +21,7 @@ pub struct Named { pub(super) name: Arc, /// Checks for a fallback if the icon was not found. - pub fallback: bool, + pub fallback: Option, /// Restrict the lookup to a given scale. #[setters(strip_option)] @@ -34,7 +44,7 @@ impl Named { Self { symbolic: name.ends_with("-symbolic"), name, - fallback: true, + fallback: Some(IconFallback::Default), size: None, scale: None, prefer_svg: false, @@ -45,6 +55,7 @@ impl Named { #[must_use] pub fn path(self) -> Option { let mut name = &*self.name; + let fallback = &self.fallback; crate::icon_theme::DEFAULT.with(|theme| { let theme = theme.borrow(); @@ -71,12 +82,22 @@ impl Named { let mut result = locate(); // On failure, attempt to locate fallback icon. - if result.is_none() && self.fallback { - for new_name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - name = new_name; - result = locate(); - if result.is_some() { - break; + if result.is_none() { + if matches!(fallback, Some(IconFallback::Default)) { + for new_name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { + name = new_name; + result = locate(); + if result.is_some() { + break; + } + } + } else if let Some(IconFallback::Names(fallbacks)) = fallback { + for fallback in fallbacks { + name = fallback; + result = locate(); + if result.is_some() { + break; + } } } } From cd56266ac94d302282fd3820a16bf18691582e90 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Dec 2023 13:13:33 -0500 Subject: [PATCH 0040/1050] chore: update example --- examples/cosmic/src/window/demo.rs | 2 +- iced | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 6329dd31..adc2fb73 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -433,7 +433,7 @@ impl State { .icon(), icon::from_name("microphone-sensitivity-high-symbolic-test") .size(24) - .fallback(false) + .fallback(None) .icon(), ]) .layer(cosmic_theme::Layer::Primary) diff --git a/iced b/iced index 611ce160..51723e87 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 611ce160e6b2ccab388fc8e303d4795a3365afbc +Subproject commit 51723e87f7de6f9ce8943c28acd0ab62270675f6 From ef657fb19de8f13d4d0a853e0c15ea5d136fa88a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 Dec 2023 15:37:43 -0500 Subject: [PATCH 0041/1050] fix: icon button should not use on_accent color for text when hovered --- src/theme/style/button.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index ec263eb5..8ec901de 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -76,11 +76,6 @@ pub fn appearance( appearance.background = Some(Background::Color(background)); appearance.text_color = text; appearance.icon_color = icon; - - if focused { - appearance.text_color = Some(cosmic.accent.on.into()); - appearance.icon_color = Some(cosmic.accent.on.into()); - } } Button::Image => { From a4d1b1b651d763dba51c17037588ede9fc084b5c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 12 Dec 2023 19:53:17 -0500 Subject: [PATCH 0042/1050] refactor: cosmic-config granular key updates and remove unused generics from cosmic-theme --- cosmic-config-derive/Cargo.toml | 2 +- cosmic-config-derive/src/lib.rs | 46 +++-- cosmic-config/src/lib.rs | 41 ++--- cosmic-theme/src/lib.rs | 3 - cosmic-theme/src/model/cosmic_palette.rs | 186 ++++++-------------- cosmic-theme/src/model/dark.ron | 117 +------------ cosmic-theme/src/model/derivation.rs | 101 ++++++----- cosmic-theme/src/model/light.ron | 117 +------------ cosmic-theme/src/model/theme.rs | 214 ++++------------------- cosmic-theme/src/util.rs | 33 ---- examples/multi-window/src/window.rs | 3 +- src/theme/mod.rs | 20 +-- src/theme/style/button.rs | 4 +- src/theme/style/iced.rs | 3 +- src/theme/style/segmented_button.rs | 25 +-- src/widget/context_drawer/overlay.rs | 2 +- src/widget/menu/menu_inner.rs | 4 +- src/widget/spin_button/model.rs | 11 ++ 18 files changed, 233 insertions(+), 699 deletions(-) delete mode 100644 cosmic-theme/src/util.rs diff --git a/cosmic-config-derive/Cargo.toml b/cosmic-config-derive/Cargo.toml index 44f960ec..46d79658 100644 --- a/cosmic-config-derive/Cargo.toml +++ b/cosmic-config-derive/Cargo.toml @@ -9,4 +9,4 @@ proc-macro = true [dependencies] syn = "1.0" -quote = "1.0" +quote = "1.0" \ No newline at end of file diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 0e27afbb..76db7522 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -14,7 +14,6 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; - // let generics = &ast.generics; // Get the fields of the struct let fields = match ast.data { @@ -43,20 +42,25 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { } }); - // // Get the existing where clause or create a new one if it doesn't exist - // let mut where_clause = ast - // .generics - // .where_clause - // .clone() - // .unwrap_or_else(|| parse_quote!(where)); - - // // Add your additional constraints to the where clause - // // Here, we add the constraint 'T: Debug' to all generic parameters - // for param in ast.generics.params.iter() { - // where_clause - // .predicates - // .push(parse_quote!(#param: ::std::default::Default + ::serde::Serialize + ::serde::de::DeserializeOwned)); - // } + let update_each_config_field = fields.iter().map(|field| { + let field_name = &field.ident; + let field_type = &field.ty; + quote! { + stringify!(#field_name) => { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(value) => { + if self.#field_name != value { + keys.push(stringify!(#field_name)); + } + self.#field_name = value; + }, + Err(e) => { + errors.push(e); + } + } + } + } + }); let gen = quote! { impl CosmicConfigEntry for #name { @@ -78,6 +82,18 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { Err((errors, default)) } } + + fn update_keys>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec, Vec<&str>){ + let mut keys = Vec::with_capacity(changed_keys.len()); + let mut errors = Vec::new(); + for key in changed_keys.iter() { + match key.as_ref() { + #(#update_each_config_field)* + _ => (), + } + } + (errors, keys) + } } }; diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 8ff3d8db..203b6da7 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -325,7 +325,7 @@ impl<'a> ConfigSet for ConfigTransaction<'a> { #[cfg(feature = "subscription")] pub enum ConfigState { Init(Cow<'static, str>, u64, bool), - Waiting(T, RecommendedWatcher, mpsc::Receiver<()>, Config), + Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), Failed, } @@ -342,6 +342,12 @@ where { fn write_entry(&self, config: &Config) -> Result<(), crate::Error>; fn get_entry(config: &Config) -> Result, Self)>; + /// Returns the keys that were updated + fn update_keys>( + &mut self, + config: &Config, + changed_keys: &[T], + ) -> (Vec, Vec<&str>); } #[cfg(feature = "subscription")] @@ -409,9 +415,9 @@ async fn start_listening< Ok(c) => c, Err(_) => return ConfigState::Failed, }; - let watcher = match config.watch(move |_helper, _keys| { + let watcher = match config.watch(move |_helper, keys| { let mut tx = tx.clone(); - let _ = tx.try_send(()); + let _ = tx.try_send(keys.to_vec()); }) { Ok(w) => w, Err(_) => return ConfigState::Failed, @@ -428,24 +434,19 @@ async fn start_listening< } } } - ConfigState::Waiting(mut old, watcher, mut rx, config) => match rx.next().await { - Some(_) => match T::get_entry(&config) { - Ok(t) => { - if t != old { - old = t; - _ = output.send((id, Ok(old.clone()))).await; - } - ConfigState::Waiting(old, watcher, rx, config) - } - Err((errors, t)) => { - if t != old { - old = t; - _ = output.send((id, Err((errors, old.clone())))).await; - } - ConfigState::Waiting(old, watcher, rx, config) - } - }, + ConfigState::Waiting(mut conf_data, watcher, mut rx, config) => match rx.next().await { + Some(keys) => { + let (errors, changed) = conf_data.update_keys(&config, &keys); + if !changed.is_empty() { + if errors.is_empty() { + _ = output.send((id, Ok(conf_data.clone()))).await; + } else { + _ = output.send((id, Err((errors, conf_data.clone())))).await; + } + } + ConfigState::Waiting(conf_data, watcher, rx, config) + } None => ConfigState::Failed, }, ConfigState::Failed => pending().await, diff --git a/cosmic-theme/src/lib.rs b/cosmic-theme/src/lib.rs index 782d15f1..6c46cb30 100644 --- a/cosmic-theme/src/lib.rs +++ b/cosmic-theme/src/lib.rs @@ -7,7 +7,6 @@ //! pub use model::*; -pub use output::*; mod model; mod output; @@ -16,8 +15,6 @@ mod output; pub mod composite; /// get color steps pub mod steps; -/// utilities -pub mod util; /// name of cosmic theme pub const NAME: &'static str = "com.system76.CosmicTheme"; diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 61092081..6933c996 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,36 +1,32 @@ -use std::fmt; - use lazy_static::lazy_static; use palette::Srgba; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; - -use crate::util::CssColor; +use serde::{Deserialize, Serialize}; lazy_static! { /// built in light palette - pub static ref LIGHT_PALETTE: CosmicPalette = + pub static ref LIGHT_PALETTE: CosmicPalette = ron::from_str(include_str!("light.ron")).unwrap(); /// built in dark palette - pub static ref DARK_PALETTE: CosmicPalette = + pub static ref DARK_PALETTE: CosmicPalette = ron::from_str(include_str!("dark.ron")).unwrap(); } /// Palette type -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub enum CosmicPalette { +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +pub enum CosmicPalette { /// Dark mode - Dark(CosmicPaletteInner), + Dark(CosmicPaletteInner), /// Light mode - Light(CosmicPaletteInner), + Light(CosmicPaletteInner), /// High contrast light mode - HighContrastLight(CosmicPaletteInner), + HighContrastLight(CosmicPaletteInner), /// High contrast dark mode - HighContrastDark(CosmicPaletteInner), + HighContrastDark(CosmicPaletteInner), } -impl CosmicPalette { +impl CosmicPalette { /// extract the inner palette - pub fn inner(self) -> CosmicPaletteInner { + pub fn inner(self) -> CosmicPaletteInner { match self { CosmicPalette::Dark(p) => p, CosmicPalette::Light(p) => p, @@ -40,8 +36,8 @@ impl CosmicPalette { } } -impl AsMut> for CosmicPalette { - fn as_mut(&mut self) -> &mut CosmicPaletteInner { +impl AsMut for CosmicPalette { + fn as_mut(&mut self) -> &mut CosmicPaletteInner { match self { CosmicPalette::Dark(p) => p, CosmicPalette::Light(p) => p, @@ -51,11 +47,8 @@ impl AsMut> for CosmicPalette { } } -impl AsRef> for CosmicPalette -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ - fn as_ref(&self) -> &CosmicPaletteInner { +impl AsRef for CosmicPalette { + fn as_ref(&self) -> &CosmicPaletteInner { match self { CosmicPalette::Dark(p) => p, CosmicPalette::Light(p) => p, @@ -65,10 +58,7 @@ where } } -impl CosmicPalette -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ +impl CosmicPalette { /// check if the palette is dark pub fn is_dark(&self) -> bool { match self { @@ -86,156 +76,105 @@ where } } -impl Default for CosmicPalette -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ +impl Default for CosmicPalette { fn default() -> Self { CosmicPalette::Dark(Default::default()) } } /// The palette for Cosmic Theme, from which all color properties are derived -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct CosmicPaletteInner { +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct CosmicPaletteInner { /// name of the palette pub name: String, /// basic palette /// blue: colors used for various points of emphasis in the UI - pub blue: C, + pub blue: Srgba, /// red: colors used for various points of emphasis in the UI - pub red: C, + pub red: Srgba, /// green: colors used for various points of emphasis in the UI - pub green: C, + pub green: Srgba, /// yellow: colors used for various points of emphasis in the UI - pub yellow: C, + pub yellow: Srgba, /// surface grays /// colors used for three levels of surfaces in the UI - pub gray_1: C, + pub gray_1: Srgba, /// colors used for three levels of surfaces in the UI - pub gray_2: C, + pub gray_2: Srgba, /// colors used for three levels of surfaces in the UI - pub gray_3: C, + pub gray_3: Srgba, /// System Neutrals /// A wider spread of dark colors for more general use. - pub neutral_0: C, + pub neutral_0: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_1: C, + pub neutral_1: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_2: C, + pub neutral_2: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_3: C, + pub neutral_3: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_4: C, + pub neutral_4: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_5: C, + pub neutral_5: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_6: C, + pub neutral_6: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_7: C, + pub neutral_7: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_8: C, + pub neutral_8: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_9: C, + pub neutral_9: Srgba, /// A wider spread of dark colors for more general use. - pub neutral_10: C, + pub neutral_10: Srgba, // Utility Colors /// Utility bright green - pub bright_green: C, + pub bright_green: Srgba, /// Utility bright red - pub bright_red: C, + pub bright_red: Srgba, /// Utility bright orange - pub bright_orange: C, + pub bright_orange: Srgba, /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_warm_grey: C, + pub ext_warm_grey: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_orange: C, + pub ext_orange: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_yellow: C, + pub ext_yellow: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_blue: C, + pub ext_blue: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_purple: C, + pub ext_purple: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_pink: C, + pub ext_pink: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. - pub ext_indigo: C, + pub ext_indigo: Srgba, /// Potential Accent Color Combos - pub accent_blue: C, + pub accent_blue: Srgba, /// Potential Accent Color Combos - pub accent_red: C, + pub accent_red: Srgba, /// Potential Accent Color Combos - pub accent_green: C, + pub accent_green: Srgba, /// Potential Accent Color Combos - pub accent_warm_grey: C, + pub accent_warm_grey: Srgba, /// Potential Accent Color Combos - pub accent_orange: C, + pub accent_orange: Srgba, /// Potential Accent Color Combos - pub accent_yellow: C, + pub accent_yellow: Srgba, /// Potential Accent Color Combos - pub accent_purple: C, + pub accent_purple: Srgba, /// Potential Accent Color Combos - pub accent_pink: C, + pub accent_pink: Srgba, /// Potential Accent Color Combos - pub accent_indigo: C, + pub accent_indigo: Srgba, } -impl From> for CosmicPaletteInner { - fn from(p: CosmicPaletteInner) -> Self { - CosmicPaletteInner { - name: p.name, - blue: p.blue.into(), - red: p.red.into(), - green: p.green.into(), - yellow: p.yellow.into(), - gray_1: p.gray_1.into(), - gray_2: p.gray_2.into(), - gray_3: p.gray_3.into(), - neutral_0: p.neutral_0.into(), - neutral_1: p.neutral_1.into(), - neutral_2: p.neutral_2.into(), - neutral_3: p.neutral_3.into(), - neutral_4: p.neutral_4.into(), - neutral_5: p.neutral_5.into(), - neutral_6: p.neutral_6.into(), - neutral_7: p.neutral_7.into(), - neutral_8: p.neutral_8.into(), - neutral_9: p.neutral_9.into(), - neutral_10: p.neutral_10.into(), - bright_green: p.bright_green.into(), - bright_red: p.bright_red.into(), - bright_orange: p.bright_orange.into(), - ext_warm_grey: p.ext_warm_grey.into(), - ext_orange: p.ext_orange.into(), - ext_yellow: p.ext_yellow.into(), - ext_blue: p.ext_blue.into(), - ext_purple: p.ext_purple.into(), - ext_pink: p.ext_pink.into(), - ext_indigo: p.ext_indigo.into(), - accent_blue: p.accent_blue.into(), - accent_red: p.accent_red.into(), - accent_green: p.accent_green.into(), - accent_warm_grey: p.accent_warm_grey.into(), - accent_orange: p.accent_orange.into(), - accent_yellow: p.accent_yellow.into(), - accent_purple: p.accent_purple.into(), - accent_pink: p.accent_pink.into(), - accent_indigo: p.accent_indigo.into(), - } - } -} - -impl CosmicPalette -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ +impl CosmicPalette { /// name of the palette pub fn name(&self) -> &str { match &self { @@ -246,14 +185,3 @@ where } } } - -impl Into> for CosmicPalette { - fn into(self) -> CosmicPalette { - match self { - CosmicPalette::Dark(p) => CosmicPalette::Dark(p.into()), - CosmicPalette::Light(p) => CosmicPalette::Light(p.into()), - CosmicPalette::HighContrastLight(p) => CosmicPalette::HighContrastLight(p.into()), - CosmicPalette::HighContrastDark(p) => CosmicPalette::HighContrastDark(p.into()), - } - } -} diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index b24cea49..604e4427 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1,116 +1 @@ -Dark ( - ( - name: "cosmic-dark", - blue: ( - c: "#94EBEB", - ), - red: ( - c: "#FFB5B5", - ), - green: ( - c: "#ACF7D2", - ), - yellow: ( - c: "#FFF19E", - ), - gray_1: ( - c: "#1B1B1B", - ), - gray_2: ( - c: "#262626", - ), - gray_3: ( - c: "#303030", - ), - neutral_0: ( - c: "#000000", - ), - neutral_1: ( - c: "#1B1B1B", - ), - neutral_2: ( - c: "#303030", - ), - neutral_3: ( - c: "#474747", - ), - neutral_4: ( - c: "#5E5E5E", - ), - neutral_5: ( - c: "#777777", - ), - neutral_6: ( - c: "#919191", - ), - neutral_7: ( - c: "#ABABAB", - ), - neutral_8: ( - c: "#C6C6C6", - ), - neutral_9: ( - c: "#E2E2E2", - ), - neutral_10: ( - c: "#FFFFFF", - ), - bright_green: ( - c: "#5EDB8C", - ), - bright_red: ( - c: "#FFA090", - ), - bright_orange: ( - c: "#FFA37D", - ), - ext_warm_grey: ( - c: "#9B8E8A", - ), - ext_orange: ( - c: "#FFAD00", - ), - ext_yellow: ( - c: "#FEDB40", - ), - ext_blue: ( - c: "#48B9C7", - ), - ext_purple: ( - c: "#CF7DFF", - ), - ext_pink: ( - c: "#F93A83", - ), - ext_indigo: ( - c: "#3E88FF", - ), - accent_blue: ( - c: "#63D0DF", - ), - accent_green: ( - c: "#92CF9C", - ), - accent_warm_grey: ( - c: "#CABAB4", - ), - accent_orange: ( - c: "#FFAD00", - ), - accent_yellow: ( - c: "#F7E062", - ), - accent_purple: ( - c: "#E79CFE", - ), - accent_pink: ( - c: "#FF9CB1", - ), - accent_red: ( - c: "#FDA1A0", - ), - accent_indigo: ( - c: "#A1C0EB", - ), - ) -) +Dark((name:"cosmic-dark",blue:(red:0.5803922,green:0.92156863,blue:0.92156863,alpha:1.0),red:(red:1.0,green:0.70980394,blue:0.70980394,alpha:1.0),green:(red:0.6745098,green:0.96862745,blue:0.8235294,alpha:1.0),yellow:(red:1.0,green:0.94509804,blue:0.61960787,alpha:1.0),gray_1:(red:0.105882354,green:0.105882354,blue:0.105882354,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),gray_3:(red:0.1882353,green:0.1882353,blue:0.1882353,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.105882354,green:0.105882354,blue:0.105882354,alpha:1.0),neutral_2:(red:0.1882353,green:0.1882353,blue:0.1882353,alpha:1.0),neutral_3:(red:0.2784314,green:0.2784314,blue:0.2784314,alpha:1.0),neutral_4:(red:0.36862746,green:0.36862746,blue:0.36862746,alpha:1.0),neutral_5:(red:0.46666667,green:0.46666667,blue:0.46666667,alpha:1.0),neutral_6:(red:0.5686275,green:0.5686275,blue:0.5686275,alpha:1.0),neutral_7:(red:0.67058825,green:0.67058825,blue:0.67058825,alpha:1.0),neutral_8:(red:0.7764706,green:0.7764706,blue:0.7764706,alpha:1.0),neutral_9:(red:0.8862745,green:0.8862745,blue:0.8862745,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),bright_green:(red:0.36862746,green:0.85882354,blue:0.54901963,alpha:1.0),bright_red:(red:1.0,green:0.627451,blue:0.5647059,alpha:1.0),bright_orange:(red:1.0,green:0.6392157,blue:0.49019608,alpha:1.0),ext_warm_grey:(red:0.60784316,green:0.5568628,blue:0.5411765,alpha:1.0),ext_orange:(red:1.0,green:0.6784314,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882354,blue:0.2509804,alpha:1.0),ext_blue:(red:0.28235295,green:0.7254902,blue:0.78039217,alpha:1.0),ext_purple:(red:0.8117647,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.9764706,green:0.22745098,blue:0.5137255,alpha:1.0),ext_indigo:(red:0.24313726,green:0.53333336,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.8156863,blue:0.8745098,alpha:1.0),accent_red:(red:0.99215686,green:0.6313726,blue:0.627451,alpha:1.0),accent_green:(red:0.57254905,green:0.8117647,blue:0.6117647,alpha:1.0),accent_warm_grey:(red:0.7921569,green:0.7294118,blue:0.7058824,alpha:1.0),accent_orange:(red:1.0,green:0.6784314,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.8784314,blue:0.38431373,alpha:1.0),accent_purple:(red:0.90588236,green:0.6117647,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.6117647,blue:0.69411767,alpha:1.0),accent_indigo:(red:0.6313726,green:0.7529412,blue:0.92156863,alpha:1.0))) \ No newline at end of file diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index 1fd4fb8d..599a0861 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,28 +1,24 @@ use palette::Srgba; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::fmt; +use serde::{Deserialize, Serialize}; use crate::composite::over; /// Theme Container colors of a theme, can be a theme background container, primary container, or secondary container -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct Container { +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +pub struct Container { /// the color of the container - pub base: C, + pub base: Srgba, /// the color of components in the container - pub component: Component, + pub component: Component, /// the color of dividers in the container - pub divider: C, + pub divider: Srgba, /// the color of text in the container - pub on: C, + pub on: Srgba, } -impl Container -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ +impl Container { /// convert to srgba - pub fn into_srgba(self) -> Container { + pub fn into_srgba(self) -> Container { Container { base: self.base.into(), component: self.component.into_srgba(), @@ -31,7 +27,7 @@ where } } - pub(crate) fn new(component: Component, bg: C, on_bg: C) -> Self { + pub(crate) fn new(component: Component, bg: Srgba, on_bg: Srgba) -> Self { let mut divider_c: Srgba = on_bg.clone().into(); divider_c.alpha = 0.2; @@ -46,40 +42,37 @@ where } /// The colors for a widget of the Cosmic theme -#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize, Eq)] -pub struct Component { +#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)] +pub struct Component { /// The base color of the widget - pub base: C, + pub base: Srgba, /// The color of the widget when it is hovered - pub hover: C, + pub hover: Srgba, /// the color of the widget when it is pressed - pub pressed: C, + pub pressed: Srgba, /// the color of the widget when it is selected - pub selected: C, + pub selected: Srgba, /// the color of the widget when it is selected - pub selected_text: C, + pub selected_text: Srgba, /// the color of the widget when it is focused - pub focus: C, + pub focus: Srgba, /// the color of dividers for this widget - pub divider: C, + pub divider: Srgba, /// the color of text for this widget - pub on: C, + pub on: Srgba, // the color of text with opacity 80 for this widget - // pub text_opacity_80: C, + // pub text_opacity_80: Srgba, /// the color of the widget when it is disabled - pub disabled: C, + pub disabled: Srgba, /// the color of text in the widget when it is disabled - pub on_disabled: C, + pub on_disabled: Srgba, /// the color of the border for the widget - pub border: C, + pub border: Srgba, /// the color of the border for the widget when it is disabled - pub disabled_border: C, + pub disabled_border: Srgba, } -impl Component -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ +impl Component { /// get @hover_state_color pub fn hover_state_color(&self) -> Srgba { self.hover.clone().into() @@ -101,7 +94,7 @@ where self.focus.clone().into() } /// convert to srgba - pub fn into_srgba(self) -> Component { + pub fn into_srgba(self) -> Component { Component { base: self.base.into(), hover: self.hover.into(), @@ -119,7 +112,13 @@ where } /// helper for producing a component from a base color a neutral and an accent - pub fn colored_component(base: C, neutral: C, accent: C, hovered: C, pressed: C) -> Self { + pub fn colored_component( + base: Srgba, + neutral: Srgba, + accent: Srgba, + hovered: Srgba, + pressed: Srgba, + ) -> Self { let base: Srgba = base.into(); let mut base_50 = base.clone(); base_50.alpha *= 0.5; @@ -146,44 +145,42 @@ where /// helper for producing a button component pub fn colored_button( - base: C, - overlay: C, - on_button: C, - accent: C, - hovered: C, - pressed: C, + base: Srgba, + overlay: Srgba, + on_button: Srgba, + accent: Srgba, + hovered: Srgba, + pressed: Srgba, ) -> Self { let mut component = Component::colored_component(base, overlay, accent, hovered, pressed); component.on = on_button.clone(); - let mut on_disabled = on_button.into(); + let mut on_disabled = on_button; on_disabled.alpha = 0.5; - component.on_disabled = on_disabled.into(); + component.on_disabled = on_disabled; component } /// helper for producing a component color theme pub fn component( - base: C, - accent: C, - on_component: C, - hovered: C, - pressed: C, + base: Srgba, + accent: Srgba, + on_component: Srgba, + hovered: Srgba, + pressed: Srgba, is_high_contrast: bool, - border: C, + border: Srgba, ) -> Self { - let base = base.into(); let mut base_50 = base.clone(); base_50.alpha *= 0.5; - let mut on_20 = on_component.clone().into(); + let mut on_20 = on_component.clone(); let mut on_50 = on_20.clone(); on_20.alpha = 0.2; on_50.alpha = 0.5; - let border = border.into(); let mut disabled_border = border; disabled_border.alpha *= 0.5; diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 07b5a64b..7de84a03 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1,116 +1 @@ -Light ( - ( - name: "cosmic-light", - blue: ( - c: "#00496D", - ), - red: ( - c: "#A0252B", - ), - green: ( - c: "#3B6E43", - ), - yellow: ( - c: "#966800", - ), - gray_1: ( - c: "#DDDDDD", - ), - gray_2: ( - c: "#E8E8E8", - ), - gray_3: ( - c: "#F3F3F3", - ), - neutral_0: ( - c: "#FFFFFF", - ), - neutral_1: ( - c: "#E2E2E2", - ), - neutral_2: ( - c: "#C6C6C6", - ), - neutral_3: ( - c: "#ABABAB", - ), - neutral_4: ( - c: "#919191", - ), - neutral_5: ( - c: "#777777", - ), - neutral_6: ( - c: "#5E5E5E", - ), - neutral_7: ( - c: "#474747", - ), - neutral_8: ( - c: "#303030", - ), - neutral_9: ( - c: "#1B1B1B", - ), - neutral_10: ( - c: "#000000", - ), - bright_green: ( - c: "#00572C", - ), - bright_red: ( - c: "#890418", - ), - bright_orange: ( - c: "#792C00", - ), - ext_warm_grey: ( - c: "#9B8E8A", - ), - ext_orange: ( - c: "#FBB86C", - ), - ext_yellow: ( - c: "#F7E062", - ), - ext_blue: ( - c: "#6ACAD8", - ), - ext_purple: ( - c: "#D58CFF", - ), - ext_pink: ( - c: "#FF9CDD", - ), - ext_indigo: ( - c: "#95C4FC", - ), - accent_blue: ( - c: "#00525A", - ), - accent_red: ( - c: "#78292E", - ), - accent_green: ( - c: "#185529", - ), - accent_warm_grey: ( - c: "#554742", - ), - accent_orange: ( - c: "#624000", - ), - accent_yellow: ( - c: "#534800", - ), - accent_purple: ( - c: "#68217C", - ), - accent_pink: ( - c: "#86043A", - ), - accent_indigo: ( - c: "#2E496D", - ), - ) -) +Light((name:"cosmic-light",blue:(red:0.0,green:0.28627452,blue:0.42745098,alpha:1.0),red:(red:0.627451,green:0.14509805,blue:0.16862746,alpha:1.0),green:(red:0.23137255,green:0.43137255,blue:0.2627451,alpha:1.0),yellow:(red:0.5882353,green:0.40784314,blue:0.0,alpha:1.0),gray_1:(red:0.8666667,green:0.8666667,blue:0.8666667,alpha:1.0),gray_2:(red:0.9098039,green:0.9098039,blue:0.9098039,alpha:1.0),gray_3:(red:0.9529412,green:0.9529412,blue:0.9529412,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.8862745,green:0.8862745,blue:0.8862745,alpha:1.0),neutral_2:(red:0.7764706,green:0.7764706,blue:0.7764706,alpha:1.0),neutral_3:(red:0.67058825,green:0.67058825,blue:0.67058825,alpha:1.0),neutral_4:(red:0.5686275,green:0.5686275,blue:0.5686275,alpha:1.0),neutral_5:(red:0.46666667,green:0.46666667,blue:0.46666667,alpha:1.0),neutral_6:(red:0.36862746,green:0.36862746,blue:0.36862746,alpha:1.0),neutral_7:(red:0.2784314,green:0.2784314,blue:0.2784314,alpha:1.0),neutral_8:(red:0.1882353,green:0.1882353,blue:0.1882353,alpha:1.0),neutral_9:(red:0.105882354,green:0.105882354,blue:0.105882354,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),bright_green:(red:0.0,green:0.34117648,blue:0.17254902,alpha:1.0),bright_red:(red:0.5372549,green:0.015686275,blue:0.09411765,alpha:1.0),bright_orange:(red:0.4745098,green:0.17254902,blue:0.0,alpha:1.0),ext_warm_grey:(red:0.60784316,green:0.5568628,blue:0.5411765,alpha:1.0),ext_orange:(red:0.9843137,green:0.72156864,blue:0.42352942,alpha:1.0),ext_yellow:(red:0.96862745,green:0.8784314,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568628,green:0.7921569,blue:0.84705883,alpha:1.0),ext_purple:(red:0.8352941,green:0.54901963,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.6117647,blue:0.8666667,alpha:1.0),ext_indigo:(red:0.58431375,green:0.76862746,blue:0.9882353,alpha:1.0),accent_blue:(red:0.0,green:0.32156864,blue:0.3529412,alpha:1.0),accent_red:(red:0.47058824,green:0.16078432,blue:0.18039216,alpha:1.0),accent_green:(red:0.09411765,green:0.33333334,blue:0.16078432,alpha:1.0),accent_warm_grey:(red:0.33333334,green:0.2784314,blue:0.25882354,alpha:1.0),accent_orange:(red:0.38431373,green:0.2509804,blue:0.0,alpha:1.0),accent_yellow:(red:0.3254902,green:0.28235295,blue:0.0,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941177,blue:0.4862745,alpha:1.0),accent_pink:(red:0.5254902,green:0.015686275,blue:0.22745098,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627452,blue:0.42745098,alpha:1.0))) \ No newline at end of file diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index f0be9d6b..d454f304 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -2,7 +2,7 @@ use crate::{ composite::over, steps::*, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, ThemeMode, DARK_PALETTE, LIGHT_PALETTE, NAME, }; -use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; +use cosmic_config::{Config, CosmicConfigEntry}; use palette::{IntoColor, Srgb, Srgba}; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; @@ -32,42 +32,49 @@ pub enum Layer { } /// Cosmic Theme data structure with all colors and its name -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] -pub struct Theme { +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + cosmic_config::cosmic_config_derive::CosmicConfigEntry, +)] +pub struct Theme { /// name of the theme pub name: String, /// background element colors - pub background: Container, + pub background: Container, /// primary element colors - pub primary: Container, + pub primary: Container, /// secondary element colors - pub secondary: Container, + pub secondary: Container, /// accent element colors - pub accent: Component, + pub accent: Component, /// suggested element colors - pub success: Component, + pub success: Component, /// destructive element colors - pub destructive: Component, + pub destructive: Component, /// warning element colors - pub warning: Component, + pub warning: Component, /// accent button element colors - pub accent_button: Component, + pub accent_button: Component, /// suggested button element colors - pub success_button: Component, + pub success_button: Component, /// destructive button element colors - pub destructive_button: Component, + pub destructive_button: Component, /// warning button element colors - pub warning_button: Component, + pub warning_button: Component, /// icon button element colors - pub icon_button: Component, + pub icon_button: Component, /// link button element colors - pub link_button: Component, + pub link_button: Component, /// text button element colors - pub text_button: Component, + pub text_button: Component, /// button component styling - pub button: Component, + pub button: Component, /// palette - pub palette: CosmicPaletteInner, + pub palette: CosmicPaletteInner, /// spacing pub spacing: Spacing, /// corner radii @@ -86,149 +93,7 @@ pub struct Theme { pub is_frosted: bool, } -impl cosmic_config::CosmicConfigEntry for Theme { - fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { - let self_ = self.clone(); - // TODO do as transaction - let tx = config.transaction(); - - tx.set("name", self_.name)?; - tx.set("background", self_.background)?; - tx.set("primary", self_.primary)?; - tx.set("secondary", self_.secondary)?; - tx.set("accent", self_.accent)?; - tx.set("success", self_.success)?; - tx.set("destructive", self_.destructive)?; - tx.set("warning", self_.warning)?; - tx.set("accent_button", self_.accent_button)?; - tx.set("success_button", self_.success_button)?; - tx.set("warning_button", self_.warning_button)?; - tx.set("destructive_button", self_.destructive_button)?; - tx.set("icon_button", self_.icon_button)?; - tx.set("link_button", self_.link_button)?; - tx.set("text_button", self_.text_button)?; - tx.set("button", self_.button)?; - tx.set("palette", self_.palette)?; - tx.set("is_dark", self_.is_dark)?; - tx.set("is_high_contrast", self_.is_high_contrast)?; - tx.set("spacing", self_.spacing)?; - tx.set("corner_radii", self_.corner_radii)?; - tx.set("active_hint", self_.active_hint)?; - tx.set("gaps", self_.gaps)?; - tx.set("window_hint", self_.window_hint)?; - - tx.commit() - } - - fn get_entry(config: &Config) -> Result, Self)> { - let mut default = Self::default(); - let mut errors = Vec::new(); - - match config.get::("name") { - Ok(name) => default.name = name, - Err(e) => errors.push(e), - } - match config.get::>("background") { - Ok(background) => default.background = background, - Err(e) => errors.push(e), - } - match config.get::>("primary") { - Ok(primary) => default.primary = primary, - Err(e) => errors.push(e), - } - match config.get::>("secondary") { - Ok(secondary) => default.secondary = secondary, - Err(e) => errors.push(e), - } - match config.get::>("accent") { - Ok(accent) => default.accent = accent, - Err(e) => errors.push(e), - } - match config.get::>("success") { - Ok(success) => default.success = success, - Err(e) => errors.push(e), - } - match config.get::>("destructive") { - Ok(destructive) => default.destructive = destructive, - Err(e) => errors.push(e), - } - match config.get::>("warning") { - Ok(warning) => default.warning = warning, - Err(e) => errors.push(e), - } - match config.get::>("success_button") { - Ok(b) => default.success_button = b, - Err(e) => errors.push(e), - } - match config.get::>("accent_button") { - Ok(b) => default.accent_button = b, - Err(e) => errors.push(e), - } - match config.get::>("destructive_button") { - Ok(b) => default.destructive_button = b, - Err(e) => errors.push(e), - } - match config.get::>("warning_button") { - Ok(warning) => default.warning_button = warning, - Err(e) => errors.push(e), - } - match config.get::>("icon_button") { - Ok(b) => default.link_button = b, - Err(e) => errors.push(e), - } - match config.get::>("link_button") { - Ok(b) => default.link_button = b, - Err(e) => errors.push(e), - } - match config.get::>("text_button") { - Ok(b) => default.text_button = b, - Err(e) => errors.push(e), - } - match config.get::>("button") { - Ok(b) => default.button = b, - Err(e) => errors.push(e), - } - match config.get::>("palette") { - Ok(palette) => default.palette = palette, - Err(e) => errors.push(e), - } - match config.get::("is_dark") { - Ok(is_dark) => default.is_dark = is_dark, - Err(e) => errors.push(e), - } - match config.get::("is_high_contrast") { - Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast, - Err(e) => errors.push(e), - } - match config.get::("spacing") { - Ok(spacing) => default.spacing = spacing, - Err(e) => errors.push(e), - } - match config.get::("corner_radii") { - Ok(corner_radii) => default.corner_radii = corner_radii, - Err(e) => errors.push(e), - } - match config.get::("active_hint") { - Ok(active_hint) => default.active_hint = active_hint, - Err(e) => errors.push(e), - } - match config.get::<(u32, u32)>("gaps") { - Ok(gaps) => default.gaps = gaps, - Err(e) => errors.push(e), - } - match config.get::>("window_hint") { - Ok(window_hint) => default.window_hint = window_hint, - Err(e) => errors.push(e), - } - if errors.is_empty() { - Ok(default) - } else { - Err((errors, default)) - } - } -} - -impl Default for Theme { +impl Default for Theme { fn default() -> Self { Self::dark_default() } @@ -240,7 +105,7 @@ pub trait LayeredTheme { fn set_layer(&mut self, layer: Layer); } -impl Theme { +impl Theme { /// version of the theme pub fn version() -> u64 { 1 @@ -260,9 +125,7 @@ impl Theme { pub fn light_config() -> Result { Config::new(LIGHT_THEME_ID, Self::version()) } -} -impl Theme { /// get the built in light theme pub fn light_default() -> Self { LIGHT_PALETTE.clone().into() @@ -510,12 +373,9 @@ impl Theme { } } -impl From> for Theme -where - CosmicPalette: Into>, -{ - fn from(p: CosmicPalette) -> Self { - ThemeBuilder::palette(p.into()).build() +impl From for Theme { + fn from(p: CosmicPalette) -> Self { + ThemeBuilder::palette(p).build() } } @@ -530,7 +390,7 @@ where )] pub struct ThemeBuilder { /// override the palette for the builder - pub palette: CosmicPalette, + pub palette: CosmicPalette, /// override spacing for the builder pub spacing: Spacing, /// override corner radii for the builder @@ -606,7 +466,7 @@ impl ThemeBuilder { /// Get a builder that is initialized with the default dark high contrast theme pub fn dark_high_contrast() -> Self { - let palette: CosmicPalette = DARK_PALETTE.to_owned().into(); + let palette: CosmicPalette = DARK_PALETTE.to_owned().into(); Self { palette: CosmicPalette::HighContrastDark(palette.inner()), ..Default::default() @@ -615,7 +475,7 @@ impl ThemeBuilder { /// Get a builder that is initialized with the default light high contrast theme pub fn light_high_contrast() -> Self { - let palette: CosmicPalette = LIGHT_PALETTE.to_owned().into(); + let palette: CosmicPalette = LIGHT_PALETTE.to_owned().into(); Self { palette: CosmicPalette::HighContrastLight(palette.inner()), ..Default::default() @@ -623,7 +483,7 @@ impl ThemeBuilder { } /// Get a builder that is initialized with the provided palette - pub fn palette(palette: CosmicPalette) -> Self { + pub fn palette(palette: CosmicPalette) -> Self { Self { palette, ..Default::default() @@ -691,7 +551,7 @@ impl ThemeBuilder { } /// build the theme - pub fn build(self) -> Theme { + pub fn build(self) -> Theme { let Self { mut palette, spacing, @@ -861,7 +721,7 @@ impl ThemeBuilder { button_hovered_overlay.alpha = 0.2; button_pressed_overlay.alpha = 0.5; - let mut theme: Theme = Theme { + let mut theme: Theme = Theme { name: palette.name().to_string(), primary: Container::new( primary_component, diff --git a/cosmic-theme/src/util.rs b/cosmic-theme/src/util.rs deleted file mode 100644 index 762017d6..00000000 --- a/cosmic-theme/src/util.rs +++ /dev/null @@ -1,33 +0,0 @@ -use csscolorparser::Color; -use palette::Srgba; -use serde::{Deserialize, Serialize}; - -/// utility wrapper for serializing and deserializing colors with arbitrary CSS -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -pub struct CssColor { - c: Color, -} - -impl From for CssColor { - fn from(c: Srgba) -> Self { - Self { - c: Color { - r: c.red as f64, - g: c.green as f64, - b: c.blue as f64, - a: c.alpha as f64, - }, - } - } -} - -impl Into for CssColor { - fn into(self) -> Srgba { - Srgba::new( - self.c.r as f32, - self.c.g as f32, - self.c.b as f32, - self.c.a as f32, - ) - } -} diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 4c9b1407..b9b29c84 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -6,7 +6,7 @@ use cosmic::{ iced_core::{id, Alignment, Length, Point}, iced_widget::{column, container, scrollable, text, text_input}, widget::{button, cosmic_container}, - Command, + ApplicationExt, Command, }; #[derive(Debug, Clone, PartialEq)] @@ -107,6 +107,7 @@ impl cosmic::Application for MultiWindow { input_value: String::new(), }, ); + _ = self.set_window_title(format!("window_{}", count), id); spawn_window } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index db691170..0208dd61 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -12,20 +12,20 @@ use cosmic_config::CosmicConfigEntry; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; use iced_futures::Subscription; -use palette::Srgba; + use std::cell::RefCell; use std::sync::Arc; pub type CosmicColor = ::palette::rgb::Srgba; -pub type CosmicComponent = cosmic_theme::Component; -pub type CosmicTheme = cosmic_theme::Theme; +pub type CosmicComponent = cosmic_theme::Component; +pub type CosmicTheme = cosmic_theme::Theme; lazy_static::lazy_static! { pub static ref COSMIC_DARK: CosmicTheme = CosmicTheme::dark_default(); pub static ref COSMIC_HC_DARK: CosmicTheme = CosmicTheme::high_contrast_dark_default(); pub static ref COSMIC_LIGHT: CosmicTheme = CosmicTheme::light_default(); pub static ref COSMIC_HC_LIGHT: CosmicTheme = CosmicTheme::high_contrast_light_default(); - pub static ref TRANSPARENT_COMPONENT: Component = Component { + pub static ref TRANSPARENT_COMPONENT: Component = Component { base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0), @@ -69,7 +69,7 @@ pub fn is_high_contrast() -> bool { /// Watches for changes to the system's theme preference. pub fn subscription(id: u64, is_dark: bool) -> Subscription { - config_subscription::<_, crate::cosmic_theme::Theme>( + config_subscription::<_, crate::cosmic_theme::Theme>( (id, is_dark), if is_dark { cosmic_theme::DARK_THEME_ID @@ -77,7 +77,7 @@ pub fn subscription(id: u64, is_dark: bool) -> Subscription cosmic_theme::LIGHT_THEME_ID } .into(), - crate::cosmic_theme::Theme::::version(), + crate::cosmic_theme::Theme::version(), ) .map(|(_, res)| { let theme = res.unwrap_or_else(|(errors, theme)| { @@ -102,9 +102,9 @@ pub fn system_preference() -> Theme { }; let helper = if is_dark { - crate::cosmic_theme::Theme::::dark_config() + crate::cosmic_theme::Theme::dark_config() } else { - crate::cosmic_theme::Theme::::light_config() + crate::cosmic_theme::Theme::light_config() }; let Ok(helper) = helper else { @@ -164,7 +164,7 @@ pub struct Theme { impl Theme { #[must_use] - pub fn cosmic(&self) -> &cosmic_theme::Theme { + pub fn cosmic(&self) -> &cosmic_theme::Theme { match self.theme_type { ThemeType::Dark => &COSMIC_DARK, ThemeType::Light => &COSMIC_LIGHT, @@ -219,7 +219,7 @@ impl Theme { /// get current container /// can be used in a component that is intended to be a child of a `CosmicContainer` #[must_use] - pub fn current_container(&self) -> &cosmic_theme::Container { + pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { cosmic_theme::Layer::Background => &self.cosmic().background, cosmic_theme::Layer::Primary => &self.cosmic().primary, diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 8ec901de..efa0abdf 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -5,7 +5,7 @@ use cosmic_theme::Component; use iced_core::{Background, Color}; -use palette::{rgb::Rgb, Alpha}; + use crate::{ theme::TRANSPARENT_COMPONENT, @@ -40,7 +40,7 @@ pub fn appearance( theme: &crate::Theme, focused: bool, style: &Button, - color: impl Fn(&Component>) -> (Color, Option, Option), + color: impl Fn(&Component) -> (Color, Option, Option), ) -> Appearance { let cosmic = theme.cosmic(); let mut corner_radii = &cosmic.corner_radii.radius_xl; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 3624c404..603f5a73 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -520,8 +520,7 @@ impl slider::StyleSheet for Theme { Slider::Standard => //TODO: no way to set rail thickness { - let cosmic: &cosmic_theme::Theme> = - self.cosmic(); + let cosmic: &cosmic_theme::Theme = self.cosmic(); slider::Appearance { rail: Rail { diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index cd6344d1..8d6b2c12 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -6,7 +6,6 @@ use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; -use palette::{rgb::Rgb, Alpha}; #[derive(Default)] pub enum SegmentedButton { @@ -155,9 +154,8 @@ impl StyleSheet for Theme { mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; - use palette::{rgb::Rgb, white_point::C, Alpha}; - pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { + pub fn selection_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; let rad_m = cosmic.corner_radii.radius_m; @@ -180,9 +178,7 @@ mod horizontal { } } - pub fn view_switcher_active( - cosmic: &cosmic_theme::Theme>, - ) -> ItemStatusAppearance { + pub fn view_switcher_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; let rad_s = cosmic.corner_radii.radius_s; @@ -209,10 +205,7 @@ mod horizontal { } } -pub fn focus( - cosmic: &cosmic_theme::Theme>, - default: &ItemStatusAppearance, -) -> ItemStatusAppearance { +pub fn focus(cosmic: &cosmic_theme::Theme, default: &ItemStatusAppearance) -> ItemStatusAppearance { // TODO: This is a hack to make the hover color lighter than the selected color // I'm not sure why the alpha is being applied differently here than in figma let mut neutral_5 = cosmic.palette.neutral_5; @@ -224,10 +217,7 @@ pub fn focus( } } -pub fn hover( - cosmic: &cosmic_theme::Theme>, - default: &ItemStatusAppearance, -) -> ItemStatusAppearance { +pub fn hover(cosmic: &cosmic_theme::Theme, default: &ItemStatusAppearance) -> ItemStatusAppearance { let mut neutral_10 = cosmic.palette.neutral_10; neutral_10.alpha = 0.1; ItemStatusAppearance { @@ -240,9 +230,8 @@ pub fn hover( mod vertical { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; - use palette::{rgb::Rgb, Alpha}; - pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { + pub fn selection_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; let rad_0 = cosmic.corner_radii.radius_0; @@ -265,9 +254,7 @@ mod vertical { } } - pub fn view_switcher_active( - cosmic: &cosmic_theme::Theme>, - ) -> ItemStatusAppearance { + pub fn view_switcher_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; neutral_5.alpha = 0.2; ItemStatusAppearance { diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index a66f3900..1096c89d 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -34,7 +34,7 @@ where let mut node = self .content .as_widget() - .layout(&mut self.tree, renderer, &limits); + .layout(self.tree, renderer, &limits); let node_size = node.size(); node.move_to(Point { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index a0f3439c..a731c98d 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -484,8 +484,8 @@ where (root, Vec::new()), |(menu_root, mut nodes), (_i, ms)| { let slice = ms.slice(bounds, overlay_offset, self.item_height); - let start_index = slice.start_index; - let end_index = slice.end_index; + let _start_index = slice.start_index; + let _end_index = slice.end_index; let children_node = ms.layout( overlay_offset, slice, diff --git a/src/widget/spin_button/model.rs b/src/widget/spin_button/model.rs index 2f4c7efa..e617bc87 100644 --- a/src/widget/spin_button/model.rs +++ b/src/widget/spin_button/model.rs @@ -133,6 +133,17 @@ impl Default for Model { } } +impl Default for Model { + fn default() -> Self { + Self { + value: 0, + step: 1, + min: u64::MIN, + max: u64::MAX, + } + } +} + impl Default for Model { fn default() -> Self { Self { From 06c33dcf06178963a31467f155252afc7233aaa2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 15 Dec 2023 17:00:08 -0500 Subject: [PATCH 0043/1050] refactor: optional config subscriptions using dbus --- Cargo.toml | 3 + cosmic-config-derive/src/lib.rs | 24 +++++- cosmic-config/Cargo.toml | 6 +- cosmic-config/src/dbus.rs | 137 ++++++++++++++++++++++++++++++ cosmic-config/src/lib.rs | 135 +++-------------------------- cosmic-config/src/subscription.rs | 119 ++++++++++++++++++++++++++ cosmic-theme/src/model/mode.rs | 1 + cosmic-theme/src/model/theme.rs | 2 + examples/cosmic/Cargo.toml | 2 +- src/app/core.rs | 17 ++++ src/app/cosmic.rs | 58 +++++++++++-- src/app/mod.rs | 2 +- src/applet/mod.rs | 4 +- src/theme/mod.rs | 10 ++- 14 files changed, 381 insertions(+), 139 deletions(-) create mode 100644 cosmic-config/src/dbus.rs create mode 100644 cosmic-config/src/subscription.rs diff --git a/Cargo.toml b/Cargo.toml index c404206d..bca811bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ xdg-portal = ["ashpd"] applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] applet-token = [] single-instance = ["dep:zbus", "serde", "ron"] +dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"] [dependencies] apply = "0.3.0" @@ -68,6 +69,8 @@ css-color = "0.2.5" nix = { version = "0.27", features = ["process"], optional = true } zbus = {version = "3.14.1", default-features = false, optional = true} serde = { version = "1.0.180", optional = true } +cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } + [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.4" diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 76db7522..69fd6e96 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{self}; -#[proc_macro_derive(CosmicConfigEntry)] +#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -13,6 +13,24 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { } fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { + let attributes = &ast.attrs; + let version = attributes + .iter() + .find_map(|attr| { + if attr.path.is_ident("version") { + match attr.parse_meta() { + Ok(syn::Meta::NameValue(syn::MetaNameValue { + lit: syn::Lit::Int(lit_int), + .. + })) => Some(lit_int.base10_parse::().unwrap()), + _ => None, + } + } else { + None + } + }) + .unwrap_or(0); + let name = &ast.ident; // Get the fields of the struct @@ -64,6 +82,8 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let gen = quote! { impl CosmicConfigEntry for #name { + const VERSION: u64 = #version; + fn write_entry(&self, config: &cosmic_config::Config) -> Result<(), cosmic_config::Error> { let tx = config.transaction(); #(#write_each_config_field)* @@ -83,7 +103,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { } } - fn update_keys>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec, Vec<&str>){ + fn update_keys>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec, Vec<&'static str>){ let mut keys = Vec::with_capacity(changed_keys.len()); let mut errors = Vec::new(); for key in changed_keys.iter() { diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 3593ae38..154183e4 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -5,11 +5,13 @@ edition = "2021" [features] default = ["macro", "subscription"] +dbus = ["dep:zbus", "cosmic-settings-daemon", "futures-util", "subscription"] macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] # For redox support +zbus = { version = "3.14.1", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.12.2", optional = true } dirs = "5.0.1" @@ -19,4 +21,6 @@ serde = "1.0.152" 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 } - +once_cell = "1.19.0" +cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } +futures-util = { version = "0.3", optional = true } diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs new file mode 100644 index 00000000..b048660c --- /dev/null +++ b/cosmic-config/src/dbus.rs @@ -0,0 +1,137 @@ +use std::ops::Deref; + +use crate::CosmicConfigEntry; +use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy, Ping}; +use futures_util::SinkExt; +use iced_futures::futures::{future::pending, StreamExt}; +pub async fn settings_daemon_proxy() -> zbus::Result> { + let conn = zbus::Connection::session().await?; + CosmicSettingsDaemonProxy::new(&conn).await +} + +#[derive(Debug)] +pub struct Watcher { + proxy: ConfigProxy<'static>, +} + +impl Deref for Watcher { + type Target = ConfigProxy<'static>; + fn deref(&self) -> &Self::Target { + &self.proxy + } +} + +impl Watcher { + pub async fn new_config( + settings_daemon_proxy: &CosmicSettingsDaemonProxy<'static>, + id: &str, + version: u64, + ) -> zbus::Result { + let (path, name) = settings_daemon_proxy.watch_config(id, version).await?; + ConfigProxy::builder(settings_daemon_proxy.connection()) + .path(path)? + .destination(name)? + .build() + .await + .map(|proxy| Self { proxy }) + } +} + +#[derive(Debug)] +pub struct ConfigUpdate { + pub errors: Vec, + pub keys: Vec<&'static str>, + pub config: T, +} + +pub fn watcher_subscription( + settings_daemon: CosmicSettingsDaemonProxy<'static>, + config_id: &'static str, +) -> iced_futures::Subscription> { + let id = std::any::TypeId::of::(); + iced_futures::subscription::channel((config_id, id), 5, move |mut tx| async move { + let version = T::VERSION; + let Ok(cosmic_config) = crate::Config::new(config_id, version) else { + pending::<()>().await; + unreachable!(); + }; + dbg!(config_id, version, &cosmic_config); + let mut config = match T::get_entry(&cosmic_config) { + Ok(config) => config, + Err((errors, default)) => { + if !errors.is_empty() { + eprintln!("Failed to get config: {errors:?}"); + } + default + } + }; + if let Err(err) = tx + .send(ConfigUpdate { + errors: Vec::new(), + keys: Vec::new(), + config: config.clone(), + }) + .await + { + eprintln!("Failed to send config: {err}"); + } + + dbg!("sent init"); + + let Ok(watcher) = Watcher::new_config(&settings_daemon, config_id, version).await else { + dbg!("failed to create watcher"); + pending::<()>().await; + unreachable!(); + }; + + dbg!("watcher created"); + + loop { + let Ok(changes) = watcher.receive_changed().await else { + pending::<()>().await; + unreachable!(); + }; + let Ok(pings) = watcher.receive_ping().await else { + pending::<()>().await; + unreachable!(); + }; + let mut streams = futures_util::stream_select!( + changes.map(Message::ConfigChanged), + pings.map(Message::ConfigPing) + ); + while let Some(v) = streams.next().await { + match v { + Message::ConfigChanged(change) => { + let Ok(args) = change.args() else { + continue; + }; + let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); + if !keys.is_empty() { + if let Err(err) = tx + .send(ConfigUpdate { + errors, + keys, + config: config.clone(), + }) + .await + { + eprintln!("Failed to send config update: {err}"); + } + } + } + Message::ConfigPing(_) => { + // send pong + if let Err(err) = watcher.pong().await { + eprintln!("Failed to send pong: {err}"); + } + } + } + } + } + }) +} + +pub enum Message { + ConfigChanged(Changed), + ConfigPing(Ping), +} diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 203b6da7..e7c7f5b2 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,20 +1,22 @@ -use iced_futures::futures::SinkExt; -#[cfg(feature = "subscription")] -use iced_futures::{futures::channel::mpsc, subscription}; use notify::{ event::{EventKind, ModifyKind}, - RecommendedWatcher, Watcher, + Watcher, }; use serde::{de::DeserializeOwned, Serialize}; use std::{ - borrow::Cow, fmt, fs, - hash::Hash, io::Write, path::{Path, PathBuf}, sync::Mutex, }; +#[cfg(feature = "subscription")] +mod subscription; +pub use subscription::*; + +#[cfg(all(feature = "dbus", feature = "subscription"))] +pub mod dbus; + #[cfg(feature = "macro")] pub use cosmic_config_derive; @@ -322,24 +324,12 @@ impl<'a> ConfigSet for ConfigTransaction<'a> { } } -#[cfg(feature = "subscription")] -pub enum ConfigState { - Init(Cow<'static, str>, u64, bool), - Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), - Failed, -} - -#[cfg(feature = "subscription")] -pub enum ConfigUpdate { - Update(T), - UpdateError(T, Vec), - Failed, -} - pub trait CosmicConfigEntry where Self: Sized, { + const VERSION: u64; + fn write_entry(&self, config: &Config) -> Result<(), crate::Error>; fn get_entry(config: &Config) -> Result, Self)>; /// Returns the keys that were updated @@ -347,108 +337,5 @@ where &mut self, config: &Config, changed_keys: &[T], - ) -> (Vec, Vec<&str>); -} - -#[cfg(feature = "subscription")] -pub fn config_subscription< - I: 'static + Copy + Send + Sync + Hash, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - id: I, - config_id: Cow<'static, str>, - config_version: u64, -) -> iced_futures::Subscription<(I, Result, T)>)> { - subscription::channel(id, 100, move |mut output| { - let config_id = config_id.clone(); - async move { - let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, false); - - loop { - state = start_listening(state, &mut output, id).await; - } - } - }) -} - -#[cfg(feature = "subscription")] -pub fn config_state_subscription< - I: 'static + Copy + Send + Sync + Hash, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - id: I, - config_id: Cow<'static, str>, - config_version: u64, -) -> iced_futures::Subscription<(I, Result, T)>)> { - subscription::channel(id, 100, move |mut output| { - let config_id = config_id.clone(); - async move { - let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, true); - - loop { - state = start_listening(state, &mut output, id).await; - } - } - }) -} - -async fn start_listening< - I: Copy, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( - state: ConfigState, - output: &mut mpsc::Sender<(I, Result, T)>)>, - id: I, -) -> ConfigState { - use iced_futures::futures::{future::pending, StreamExt}; - - match state { - ConfigState::Init(config_id, version, is_state) => { - let (tx, rx) = mpsc::channel(100); - let config = match if is_state { - Config::new_state(&config_id, version) - } else { - Config::new(&config_id, version) - } { - Ok(c) => c, - Err(_) => return ConfigState::Failed, - }; - let watcher = match config.watch(move |_helper, keys| { - let mut tx = tx.clone(); - let _ = tx.try_send(keys.to_vec()); - }) { - Ok(w) => w, - Err(_) => return ConfigState::Failed, - }; - - match T::get_entry(&config) { - Ok(t) => { - _ = output.send((id, Ok(t.clone()))).await; - ConfigState::Waiting(t, watcher, rx, config) - } - Err((errors, t)) => { - _ = output.send((id, Err((errors, t.clone())))).await; - ConfigState::Waiting(t, watcher, rx, config) - } - } - } - ConfigState::Waiting(mut conf_data, watcher, mut rx, config) => match rx.next().await { - Some(keys) => { - let (errors, changed) = conf_data.update_keys(&config, &keys); - - if !changed.is_empty() { - if errors.is_empty() { - _ = output.send((id, Ok(conf_data.clone()))).await; - } else { - _ = output.send((id, Err((errors, conf_data.clone())))).await; - } - } - ConfigState::Waiting(conf_data, watcher, rx, config) - } - None => ConfigState::Failed, - }, - ConfigState::Failed => pending().await, - } + ) -> (Vec, Vec<&'static str>); } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs new file mode 100644 index 00000000..8c4f3c6d --- /dev/null +++ b/cosmic-config/src/subscription.rs @@ -0,0 +1,119 @@ +use iced_futures::futures::SinkExt; +use iced_futures::{futures::channel::mpsc, subscription}; +use notify::RecommendedWatcher; +use std::{borrow::Cow, hash::Hash}; + +use crate::{Config, CosmicConfigEntry}; + +pub enum ConfigState { + Init(Cow<'static, str>, u64, bool), + Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), + Failed, +} + +pub enum ConfigUpdate { + Update(T), + UpdateError(T, Vec), + Failed, +} + +pub fn config_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced_futures::Subscription<(I, Result, T)>)> { + subscription::channel(id, 100, move |mut output| { + let config_id = config_id.clone(); + async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, false); + + loop { + state = start_listening(state, &mut output, id).await; + } + } + }) +} + +pub fn config_state_subscription< + I: 'static + Copy + Send + Sync + Hash, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + id: I, + config_id: Cow<'static, str>, + config_version: u64, +) -> iced_futures::Subscription<(I, Result, T)>)> { + subscription::channel(id, 100, move |mut output| { + let config_id = config_id.clone(); + async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, true); + + loop { + state = start_listening(state, &mut output, id).await; + } + } + }) +} + +async fn start_listening< + I: Copy, + T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, +>( + state: ConfigState, + output: &mut mpsc::Sender<(I, Result, T)>)>, + id: I, +) -> ConfigState { + use iced_futures::futures::{future::pending, StreamExt}; + + match state { + ConfigState::Init(config_id, version, is_state) => { + let (tx, rx) = mpsc::channel(100); + let config = match if is_state { + Config::new_state(&config_id, version) + } else { + Config::new(&config_id, version) + } { + Ok(c) => c, + Err(_) => return ConfigState::Failed, + }; + let watcher = match config.watch(move |_helper, keys| { + let mut tx = tx.clone(); + let _ = tx.try_send(keys.to_vec()); + }) { + Ok(w) => w, + Err(_) => return ConfigState::Failed, + }; + + match T::get_entry(&config) { + Ok(t) => { + _ = output.send((id, Ok(t.clone()))).await; + ConfigState::Waiting(t, watcher, rx, config) + } + Err((errors, t)) => { + _ = output.send((id, Err((errors, t.clone())))).await; + ConfigState::Waiting(t, watcher, rx, config) + } + } + } + ConfigState::Waiting(mut conf_data, watcher, mut rx, config) => match rx.next().await { + Some(keys) => { + let (errors, changed) = conf_data.update_keys(&config, &keys); + + if !changed.is_empty() { + if errors.is_empty() { + _ = output.send((id, Ok(conf_data.clone()))).await; + } else { + _ = output.send((id, Err((errors, conf_data.clone())))).await; + } + } + ConfigState::Waiting(conf_data, watcher, rx, config) + } + None => ConfigState::Failed, + }, + ConfigState::Failed => pending().await, + } +} diff --git a/cosmic-theme/src/model/mode.rs b/cosmic-theme/src/model/mode.rs index 85853dd6..d9a9b14d 100644 --- a/cosmic-theme/src/model/mode.rs +++ b/cosmic-theme/src/model/mode.rs @@ -7,6 +7,7 @@ pub const THEME_MODE_ID: &str = "com.system76.CosmicTheme.Mode"; #[derive( Debug, Clone, Copy, PartialEq, Eq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] +#[version = 1] pub struct ThemeMode { /// The theme dark mode setting. pub is_dark: bool, diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index d454f304..987314d8 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -40,6 +40,7 @@ pub enum Layer { PartialEq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] +#[version = 1] pub struct Theme { /// name of the theme pub name: String, @@ -388,6 +389,7 @@ impl From for Theme { cosmic_config::cosmic_config_derive::CosmicConfigEntry, PartialEq, )] +#[version = 1] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 7e7b466f..7332a695 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.14.0" -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config"] } once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" diff --git a/src/app/core.rs b/src/app/core.rs index 37a5fade..219fc595 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -69,6 +69,9 @@ pub struct Core { #[cfg(feature = "single-instance")] pub(crate) single_instance: bool, + + #[cfg(feature = "dbus-config")] + pub(crate) settings_daemon: Option>, } impl Default for Core { @@ -114,6 +117,8 @@ impl Default for Core { applet: crate::applet::Context::default(), #[cfg(feature = "single-instance")] single_instance: false, + #[cfg(feature = "dbus-config")] + settings_daemon: None, } } } @@ -208,4 +213,16 @@ impl Core { pub fn system_theme_mode(&self) -> ThemeMode { self.system_theme_mode } + + #[cfg(feature = "dbus-config")] + pub fn watch_config( + &self, + config_id: &'static str, + ) -> iced::Subscription> { + if let Some(settings_daemon) = self.settings_daemon.clone() { + cosmic_config::dbus::watcher_subscription(settings_daemon, config_id) + } else { + iced::Subscription::none() + } + } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 2b405f5f..4da3654e 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use std::sync::Arc; + use super::{command, Application, ApplicationExt, Core, Subscription}; use crate::theme::{self, Theme, ThemeType, THEME}; use crate::widget::nav_bar; @@ -64,6 +66,9 @@ pub enum Message { WmCapabilities(window::Id, WindowManagerCapabilities), /// Activate the application Activate(String), + #[cfg(feature = "dbus-config")] + /// dbus settings daemon setup + SettingsDaemon(zbus::Result>), } #[derive(Default)] @@ -83,6 +88,13 @@ where fn new((core, flags): Self::Flags) -> (Self, iced::Command) { let (model, command) = T::init(core, flags); + #[cfg(feature = "dbus-config")] + let command = iced::Command::batch(vec![ + command, + iced::Command::perform(cosmic_config::dbus::settings_daemon_proxy(), |p| { + super::Message::Cosmic(super::cosmic::Message::SettingsDaemon(p)) + }), + ]); (Self::new(model), command) } @@ -164,12 +176,26 @@ where keyboard_nav::subscription() .map(Message::KeyboardNav) .map(super::Message::Cosmic), - theme::subscription( - self.app.core().theme_sub_counter, - self.app.core().system_theme_mode.is_dark, - ) - .map(Message::SystemThemeChange) - .map(super::Message::Cosmic), + #[cfg(feature = "dbus-config")] + self.app + .core() + .watch_config::(if self.app.core().system_theme_mode.is_dark { + cosmic_theme::DARK_THEME_ID + } else { + cosmic_theme::LIGHT_THEME_ID + }) + .map(|update| { + for e in update.errors { + tracing::error!("{e}"); + } + Message::SystemThemeChange(crate::theme::Theme::system(Arc::new(update.config))) + }) + .map(super::Message::Cosmic), + #[cfg(not(feature = "dbus-config"))] + theme::subscription(self.app.core().system_theme_mode.is_dark) + .map(Message::SystemThemeChange) + .map(super::Message::Cosmic), + #[cfg(not(feature = "dbus-config"))] cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( 0, cosmic_theme::THEME_MODE_ID.into(), @@ -185,6 +211,17 @@ where } }) .map(super::Message::Cosmic), + #[cfg(feature = "dbus-config")] + self.app + .core() + .watch_config::(cosmic_theme::THEME_MODE_ID) + .map(|update| { + for e in update.errors { + tracing::error!("{e}"); + } + Message::SystemThemeModeChange(update.config) + }) + .map(super::Message::Cosmic), window_events.map(super::Message::Cosmic), #[cfg(feature = "single-instance")] self.app @@ -384,6 +421,15 @@ impl Cosmic { _token, ); } + #[cfg(feature = "dbus-config")] + Message::SettingsDaemon(p) => match p { + Ok(p) => { + self.app.core_mut().settings_daemon = Some(p); + } + Err(e) => { + tracing::error!("Failed to connect to settings daemon: {e}"); + } + }, } iced::Command::none() diff --git a/src/app/mod.rs b/src/app/mod.rs index 4921300c..5877d451 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -551,7 +551,7 @@ impl ApplicationExt for App { #[cfg(any(feature = "multi-window", feature = "wayland"))] fn title(&self, id: window::Id) -> &str { - self.core().title.get(&id).map(|s| s.as_str()).unwrap_or("") + self.core().title.get(&id).map_or("", |s| s.as_str()) } #[cfg(not(any(feature = "multi-window", feature = "wayland")))] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index dcfd271e..45d769fd 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -153,11 +153,11 @@ impl Context { Container::::new(Container::::new(content).style( theme::Container::custom(|theme| { let cosmic = theme.cosmic(); - + let corners = cosmic.corner_radii.clone(); Appearance { text_color: Some(cosmic.background.on.into()), background: Some(Color::from(cosmic.background.base).into()), - border_radius: cosmic.corner_radii.radius_m.into(), + border_radius: corners.radius_m.into(), border_width: 1.0, border_color: cosmic.background.divider.into(), icon_color: Some(cosmic.background.on.into()), diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 0208dd61..42a344ac 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -16,6 +16,9 @@ use iced_futures::Subscription; use std::cell::RefCell; use std::sync::Arc; +#[cfg(feature = "dbus-config")] +use cosmic_config::dbus; + pub type CosmicColor = ::palette::rgb::Srgba; pub type CosmicComponent = cosmic_theme::Component; pub type CosmicTheme = cosmic_theme::Theme; @@ -68,9 +71,12 @@ pub fn is_high_contrast() -> bool { } /// Watches for changes to the system's theme preference. -pub fn subscription(id: u64, is_dark: bool) -> Subscription { +pub fn subscription(is_dark: bool) -> Subscription { config_subscription::<_, crate::cosmic_theme::Theme>( - (id, is_dark), + ( + std::any::TypeId::of::(), + is_dark, + ), if is_dark { cosmic_theme::DARK_THEME_ID } else { From e1c53277d9ecb793551479a5d3cc4a563861dfb9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 15 Dec 2023 17:02:21 -0500 Subject: [PATCH 0044/1050] cleanup --- cosmic-config/src/dbus.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index b048660c..b2ecc272 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -55,7 +55,6 @@ pub fn watcher_subscription().await; unreachable!(); }; - dbg!(config_id, version, &cosmic_config); let mut config = match T::get_entry(&cosmic_config) { Ok(config) => config, Err((errors, default)) => { @@ -76,16 +75,11 @@ pub fn watcher_subscription().await; unreachable!(); }; - dbg!("watcher created"); - loop { let Ok(changes) = watcher.receive_changed().await else { pending::<()>().await; From 8f88833d8e5f65106256a7739f56c2fe56d5997d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 15 Dec 2023 17:05:46 -0500 Subject: [PATCH 0045/1050] cargo fmt --- src/theme/style/button.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index efa0abdf..4b3549c3 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -6,7 +6,6 @@ use cosmic_theme::Component; use iced_core::{Background, Color}; - use crate::{ theme::TRANSPARENT_COMPONENT, widget::button::{Appearance, StyleSheet}, From eff22fdb34cc608df49017d7b4893a70c15dc301 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 28 Dec 2023 18:25:12 -0500 Subject: [PATCH 0046/1050] chore(cosmic-config): update to include state and remove ping/pong --- cosmic-config/src/dbus.rs | 94 ++++++++++++++++---------------- examples/multi-window/Cargo.toml | 2 +- src/app/core.rs | 16 +++++- 3 files changed, 63 insertions(+), 49 deletions(-) diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index b2ecc272..a2c4f5af 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use crate::CosmicConfigEntry; -use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy, Ping}; +use cosmic_settings_daemon::{ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::futures::{future::pending, StreamExt}; pub async fn settings_daemon_proxy() -> zbus::Result> { @@ -35,10 +35,24 @@ impl Watcher { .await .map(|proxy| Self { proxy }) } + + pub async fn new_state( + settings_daemon_proxy: &CosmicSettingsDaemonProxy<'static>, + id: &str, + version: u64, + ) -> zbus::Result { + let (path, name) = settings_daemon_proxy.watch_state(id, version).await?; + ConfigProxy::builder(settings_daemon_proxy.connection()) + .path(path)? + .destination(name)? + .build() + .await + .map(|proxy| Self { proxy }) + } } #[derive(Debug)] -pub struct ConfigUpdate { +pub struct Update { pub errors: Vec, pub keys: Vec<&'static str>, pub config: T, @@ -47,11 +61,17 @@ pub struct ConfigUpdate { pub fn watcher_subscription( settings_daemon: CosmicSettingsDaemonProxy<'static>, config_id: &'static str, -) -> iced_futures::Subscription> { + is_state: bool, +) -> iced_futures::Subscription> { let id = std::any::TypeId::of::(); - iced_futures::subscription::channel((config_id, id), 5, move |mut tx| async move { + iced_futures::subscription::channel((is_state, config_id, id), 5, move |mut tx| async move { let version = T::VERSION; - let Ok(cosmic_config) = crate::Config::new(config_id, version) else { + + 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!(); }; @@ -65,7 +85,7 @@ pub fn watcher_subscription().await; unreachable!(); }; loop { - let Ok(changes) = watcher.receive_changed().await else { + let Ok(mut changes) = watcher.receive_changed().await else { pending::<()>().await; unreachable!(); }; - let Ok(pings) = watcher.receive_ping().await else { - pending::<()>().await; - unreachable!(); - }; - let mut streams = futures_util::stream_select!( - changes.map(Message::ConfigChanged), - pings.map(Message::ConfigPing) - ); - while let Some(v) = streams.next().await { - match v { - Message::ConfigChanged(change) => { - let Ok(args) = change.args() else { - continue; - }; - let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); - if !keys.is_empty() { - if let Err(err) = tx - .send(ConfigUpdate { - errors, - keys, - config: config.clone(), - }) - .await - { - eprintln!("Failed to send config update: {err}"); - } - } - } - Message::ConfigPing(_) => { - // send pong - if let Err(err) = watcher.pong().await { - eprintln!("Failed to send pong: {err}"); - } + while let Some(change) = changes.next().await { + let Ok(args) = change.args() else { + continue; + }; + 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 + { + eprintln!("Failed to send config update: {err}"); } } } } }) } - -pub enum Message { - ConfigChanged(Changed), - ConfigPing(Ping), -} diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 97177ce3..18e0fff2 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config"] } diff --git a/src/app/core.rs b/src/app/core.rs index 219fc595..764063d8 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -218,9 +218,21 @@ impl Core { pub fn watch_config( &self, config_id: &'static str, - ) -> iced::Subscription> { + ) -> iced::Subscription> { if let Some(settings_daemon) = self.settings_daemon.clone() { - cosmic_config::dbus::watcher_subscription(settings_daemon, config_id) + cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false) + } else { + iced::Subscription::none() + } + } + + #[cfg(feature = "dbus-config")] + pub fn watch_state( + &self, + state_id: &'static str, + ) -> iced::Subscription> { + if let Some(settings_daemon) = self.settings_daemon.clone() { + cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true) } else { iced::Subscription::none() } From a9df1667409844bbc333623705830c4da031960a Mon Sep 17 00:00:00 2001 From: danieleades <33452915+danieleades@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:13:16 +0000 Subject: [PATCH 0047/1050] chore: justfile improvement --- .github/workflows/ci.yml | 34 ++++++---------------------------- justfile | 9 +++++---- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c432f745..c308fbf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,13 +14,9 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 - name: Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true - profile: minimal components: rustfmt - default: true - name: Cargo cache uses: actions/cache@v3 with: @@ -29,10 +25,7 @@ jobs: ~/.cargo/git key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }} - name: Format - uses: actions-rs/cargo@v1 - with: - command: fmt - args: -- --check + run: cargo fmt -- --check tests: needs: @@ -69,18 +62,11 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - default: true + uses: dtolnay/rust-toolchain@stable - name: Test features - uses: actions-rs/cargo@v1 + run: cargo test --no-default-features --features "${{ matrix.features }}" env: RUST_BACKTRACE: full - with: - command: test - args: --no-default-features --features "${{ matrix.features }}" examples: needs: @@ -112,16 +98,8 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - default: true + uses: dtolnay/rust-toolchain@stable - name: Test example - uses: actions-rs/cargo@v1 + run: cargo check -p "${{ matrix.examples }}" env: RUST_BACKTRACE: full - with: - command: check - args: -p "${{ matrix.examples }}" - diff --git a/justfile b/justfile index b93bae64..18e06b51 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,5 @@ -examples := 'application cosmic cosmic-design-demo open-dialog' +examples := 'applet application config cosmic cosmic-design-demo multi-window open-dialog' +clippy_args := '-W clippy::all -W clippy::pedantic' # Check for errors and linter warnings check *args: (check-wayland args) (check-winit args) (check-examples args) @@ -6,14 +7,14 @@ check *args: (check-wayland args) (check-winit args) (check-examples args) check-examples *args: #!/bin/bash for project in {{examples}}; do - cargo check -p ${project} {{args}} + cargo clippy -p ${project} {{args}} -- {{clippy_args}} done check-wayland *args: - cargo clippy --no-deps --features="wayland,tokio" {{args}} -- -W clippy::pedantic + cargo clippy --no-deps --features="wayland,tokio" {{args}} -- {{clippy_args}} check-winit *args: - cargo clippy --no-deps --features="winit,tokio" {{args}} -- -W clippy::pedantic + cargo clippy --no-deps --features="winit,tokio" {{args}} -- {{clippy_args}} # Runs a check with JSON message format for IDE integration check-json: (check '--message-format=json') From 8cc0c3b469a5146deca9d7fd72de1c73ddd28b72 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 Jan 2024 11:35:37 -0500 Subject: [PATCH 0048/1050] chore: export IconFallback --- src/widget/icon/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 93442d42..0542505c 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -7,7 +7,7 @@ mod named; use std::ffi::OsStr; use std::sync::Arc; -pub use named::Named; +pub use named::{IconFallback, Named}; mod handle; pub use handle::{from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes, Data, Handle}; From 6d8bb88087b9011b089888ff62f4868808719064 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 2 Jan 2024 11:45:28 -0700 Subject: [PATCH 0049/1050] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 51723e87..611ce160 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 51723e87f7de6f9ce8943c28acd0ab62270675f6 +Subproject commit 611ce160e6b2ccab388fc8e303d4795a3365afbc From 5cb818a5f98e8060f393ae86c887a3fa214f9e07 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 Jan 2024 14:18:27 -0500 Subject: [PATCH 0050/1050] fix: icon fallback --- src/widget/icon/named.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 63d8e95e..736027e1 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -54,12 +54,12 @@ impl Named { #[cfg(not(windows))] #[must_use] pub fn path(self) -> Option { - let mut name = &*self.name; + let name = &*self.name; let fallback = &self.fallback; crate::icon_theme::DEFAULT.with(|theme| { let theme = theme.borrow(); - let locate = || { + let locate = |name| { let mut lookup = freedesktop_icons::lookup(name) .with_theme(theme.as_ref()) .with_cache(); @@ -79,22 +79,20 @@ impl Named { lookup.find() }; - let mut result = locate(); + let mut result = locate(name); // On failure, attempt to locate fallback icon. if result.is_none() { if matches!(fallback, Some(IconFallback::Default)) { for new_name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - name = new_name; - result = locate(); + result = locate(new_name); if result.is_some() { break; } } } else if let Some(IconFallback::Names(fallbacks)) = fallback { for fallback in fallbacks { - name = fallback; - result = locate(); + result = locate(fallback); if result.is_some() { break; } From 481cd5a0cdb86661417918bebe8ca2029bc752ab Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 3 Jan 2024 14:46:40 -0500 Subject: [PATCH 0051/1050] fix: position of input label --- examples/cosmic/src/window/demo.rs | 6 ++++++ src/widget/text_input/input.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index adc2fb73..4efe5afa 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -478,6 +478,12 @@ impl State { .size(20) .id(INPUT_ID.clone()) .into(), + cosmic::widget::text_input("", &self.entry_value) + .label("Test Input") + .on_input(Message::InputChanged) + .size(20) + .id(INPUT_ID.clone()) + .into(), self.color_picker_model .picker_button(Message::ColorPickerUpdate, None) .width(Length::Fixed(128.0)) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 1b756d13..04215d76 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1902,7 +1902,7 @@ pub fn draw<'a, Message>( line_height, shaping: text::Shaping::Advanced, }, - bounds.position(), + label_layout.bounds().position(), appearance.label_color, *viewport, ); From 47858bf0aac8404d34ed467e66fdc88da7bdc9ff Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 4 Jan 2024 22:08:26 +0100 Subject: [PATCH 0052/1050] bundle close, maximise and minimise icons --- res/icons/window-close-symbolic.svg | 3 +++ res/icons/window-maximize-symbolic.svg | 4 ++++ res/icons/window-minimize-symbolic.svg | 3 +++ src/widget/header_bar.rs | 16 ++++++++++------ 4 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 res/icons/window-close-symbolic.svg create mode 100644 res/icons/window-maximize-symbolic.svg create mode 100644 res/icons/window-minimize-symbolic.svg diff --git a/res/icons/window-close-symbolic.svg b/res/icons/window-close-symbolic.svg new file mode 100644 index 00000000..25336395 --- /dev/null +++ b/res/icons/window-close-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/icons/window-maximize-symbolic.svg b/res/icons/window-maximize-symbolic.svg new file mode 100644 index 00000000..ef66334e --- /dev/null +++ b/res/icons/window-maximize-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/icons/window-minimize-symbolic.svg b/res/icons/window-minimize-symbolic.svg new file mode 100644 index 00000000..fdcf99b4 --- /dev/null +++ b/res/icons/window-minimize-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 45bac890..cb8c5de0 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -167,10 +167,14 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { /// Creates the widget for window controls. fn window_controls(&mut self) -> Element<'a, Message> { - let icon = |name, size, on_press| { - widget::icon::from_name(name) - .size(size) + let icon = |icon_bytes, size, on_press| { + + widget::icon::from_svg_bytes( + icon_bytes, + ) + .symbolic(true) .apply(widget::button::icon) + .icon_size(size) .on_press(on_press) }; @@ -178,17 +182,17 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .push_maybe( self.on_minimize .take() - .map(|m| icon("window-minimize-symbolic", 16, m)), + .map(|m| icon(&include_bytes!("../../res/icons/window-minimize-symbolic.svg")[..], 16, m)), ) .push_maybe( self.on_maximize .take() - .map(|m| icon("window-maximize-symbolic", 16, m)), + .map(|m| icon(&include_bytes!("../../res/icons/window-maximize-symbolic.svg")[..], 16, m)), ) .push_maybe( self.on_close .take() - .map(|m| icon("window-close-symbolic", 16, m)), + .map(|m| icon(&include_bytes!("../../res/icons/window-close-symbolic.svg")[..], 16, m)), ) .spacing(8) .apply(widget::container) From 1bd39b17ae7c97934ea214028cd31c578b589e3e Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Thu, 4 Jan 2024 22:12:43 +0100 Subject: [PATCH 0053/1050] fmt --- src/widget/header_bar.rs | 41 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index cb8c5de0..3a8c1105 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -168,10 +168,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { /// Creates the widget for window controls. fn window_controls(&mut self) -> Element<'a, Message> { let icon = |icon_bytes, size, on_press| { - - widget::icon::from_svg_bytes( - icon_bytes, - ) + widget::icon::from_svg_bytes(icon_bytes) .symbolic(true) .apply(widget::button::icon) .icon_size(size) @@ -179,21 +176,27 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }; widget::row::with_capacity(3) - .push_maybe( - self.on_minimize - .take() - .map(|m| icon(&include_bytes!("../../res/icons/window-minimize-symbolic.svg")[..], 16, m)), - ) - .push_maybe( - self.on_maximize - .take() - .map(|m| icon(&include_bytes!("../../res/icons/window-maximize-symbolic.svg")[..], 16, m)), - ) - .push_maybe( - self.on_close - .take() - .map(|m| icon(&include_bytes!("../../res/icons/window-close-symbolic.svg")[..], 16, m)), - ) + .push_maybe(self.on_minimize.take().map(|m| { + icon( + &include_bytes!("../../res/icons/window-minimize-symbolic.svg")[..], + 16, + m, + ) + })) + .push_maybe(self.on_maximize.take().map(|m| { + icon( + &include_bytes!("../../res/icons/window-maximize-symbolic.svg")[..], + 16, + m, + ) + })) + .push_maybe(self.on_close.take().map(|m| { + icon( + &include_bytes!("../../res/icons/window-close-symbolic.svg")[..], + 16, + m, + ) + })) .spacing(8) .apply(widget::container) .height(Length::Fill) From 4674e4b23e80adcebb9096217dd3ff0a2c5ac15b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 5 Jan 2024 10:55:10 -0700 Subject: [PATCH 0054/1050] widget::popover: add show_popup to allow disabling overlay --- src/widget/popover.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 9617395d..8e990b31 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -28,6 +28,7 @@ pub struct Popover<'a, Message, Renderer> { // XXX Avoid refcell; improve iced overlay API? popup: RefCell>, position: Option, + show_popup: bool, } impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { @@ -39,6 +40,7 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { content: content.into(), popup: RefCell::new(popup.into()), position: None, + show_popup: true, } } @@ -47,6 +49,11 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { self } + pub fn show_popup(mut self, show_popup: bool) -> Self { + self.show_popup = show_popup; + self + } + // TODO More options for positioning similar to GdkPopup, xdg_popup } @@ -160,6 +167,10 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option> { + if !self.show_popup { + return None; + } + let bounds = layout.bounds(); let (position, centered) = match self.position { Some(relative) => ( From 0e0aed9bde8c4b0069c01ca5439205d93959af80 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Fri, 5 Jan 2024 20:57:55 +0100 Subject: [PATCH 0055/1050] fix(menu_bar): temporarily allow invalid reference cast --- src/widget/menu/menu_bar.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 00f377d8..596ffe64 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -211,6 +211,7 @@ where self.height } + #[allow(invalid_reference_casting)] fn diff(&mut self, tree: &mut Tree) { if tree.children.len() > self.menu_roots.len() { tree.children.truncate(self.menu_roots.len()); From 98d6d67ab9a895eef4fa2ab6103253f5fe9ac52b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sat, 6 Jan 2024 20:49:09 -0700 Subject: [PATCH 0056/1050] widget::popover: context menu positioning logic This keeps the popup inside the parent widget bounds, logic that is common for context menus --- src/widget/popover.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 8e990b31..ba745efe 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -237,6 +237,13 @@ where // Position is set to the center bottom of the lower widget let width = node.size().width; position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width); + } else { + // Position is using context menu logic + let size = node.size(); + position.x = position.x.clamp(0.0, bounds.width - size.width); + if position.y + size.height > bounds.height { + position.y = (position.y - size.height).clamp(0.0, bounds.height - size.height); + } } node.move_to(position); From 5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 9 Jan 2024 10:05:45 -0700 Subject: [PATCH 0057/1050] Fixes for menu - Invalidate layout when menu is opened - Align tree with menu widget when handling menu events --- src/widget/menu/menu_inner.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index a731c98d..ae9b5a38 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -552,6 +552,7 @@ where init_root_menu( self, renderer, + shell, overlay_cursor, viewport_size, overlay_offset, @@ -750,6 +751,7 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { fn init_root_menu( menu: &mut Menu<'_, '_, Message, Renderer>, renderer: &Renderer, + shell: &mut Shell<'_, Message>, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, @@ -815,6 +817,9 @@ fn init_root_menu( menu_bounds, }); + // Hack to ensure menu opens properly + shell.invalidate_layout(); + break; } } @@ -852,25 +857,20 @@ where .iter() .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]); + // widget tree + let tree = &mut tree.children[active_root].children[mt.index]; + // get layout let last_ms = &state.menu_states[indices.len() - 1]; - let last_tree = tree.children[active_root] - .children - .iter_mut() - .last() - .unwrap(); let child_node = last_ms.layout_single( overlay_offset, last_ms.index.expect("missing index within menu state."), renderer, mt, - last_tree, + tree, ); let child_layout = Layout::new(&child_node); - // widget tree - let tree = &mut tree.children[active_root].children[mt.index]; - // process only the last widget mt.item.as_widget_mut().on_event( tree, From 68c760e65203c5124844c2083224d8c83010ed1e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 9 Jan 2024 11:45:00 -0700 Subject: [PATCH 0058/1050] Allow apps to return a command when context drawer is toggled --- src/app/cosmic.rs | 1 + src/app/mod.rs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 4da3654e..e06840ca 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -332,6 +332,7 @@ impl Cosmic { Message::ContextDrawer(show) => { self.app.core_mut().window.show_context = show; + return self.app.on_context_drawer(); } Message::Drag => return command::drag(None), diff --git a/src/app/mod.rs b/src/app/mod.rs index 5877d451..0de53d9d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -440,6 +440,11 @@ where None } + // Called when context drawer is toggled + fn on_context_drawer(&mut self) -> iced::Command> { + iced::Command::none() + } + /// Called when the escape key is pressed. fn on_escape(&mut self) -> iced::Command> { iced::Command::none() From 6850e53855ad5884d89a5bd5eeeed08a0705004c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 10 Jan 2024 15:45:59 +0100 Subject: [PATCH 0059/1050] fix: when using wgpu, default to low power GPU Ensures that the integrated GPU is used by default on hybrid graphics systems; and resolves NVIDIA-related driver issues. --- src/app/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 0de53d9d..21dcf29a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -135,11 +135,28 @@ pub(crate) fn iced_settings( /// /// Returns error on application failure. pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { + #[cfg(feature = "wgpu")] + wgpu_power_pref(); + let settings = iced_settings::(settings, flags); cosmic::Cosmic::::run(settings) } +/// Default to rendering the application with the low power GPU preference. +#[cfg(feature = "wgpu")] +fn wgpu_power_pref() { + // Ignore if requested to run on NVIDIA GPU + if std::env::var("__NV_PRIME_RENDER_OFFLOAD").ok().as_deref() == Some("1") { + return; + } + + const VAR: &str = "WGPU_POWER_PREF"; + if std::env::var(VAR).is_err() { + std::env::set_var(VAR, "low"); + } +} + #[cfg(feature = "single-instance")] #[derive(Debug, Clone)] pub struct DbusActivationMessage> { From 9fb3d874e18ce36fadea931e16488fdbf28d3d02 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 10 Jan 2024 12:56:07 -0700 Subject: [PATCH 0060/1050] fix(header_bar): shrink start and end --- src/widget/header_bar.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 3a8c1105..ff0e9925 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -106,7 +106,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_items(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Left) - .width(Length::Fill), + .width(Length::Shrink), ) // If elements exist in the center region, use them here. // This will otherwise use the title as a widget if a title was defined. @@ -127,7 +127,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_items(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Right) - .width(Length::Fill), + .width(Length::Shrink), ) .height(Length::Fixed(50.0)) .padding(8) From 8157ed5c63cc617864ea1565462f193e3d5e4c8d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 11 Jan 2024 15:16:21 -0500 Subject: [PATCH 0061/1050] feat: apply overlay to headerbar when it loses focus --- examples/cosmic/src/window.rs | 1 + examples/multi-window/src/window.rs | 14 +- src/app/mod.rs | 1 + src/theme/style/iced.rs | 10 +- src/widget/header_bar.rs | 234 +++++++++++++++++++++++++++- 5 files changed, 245 insertions(+), 15 deletions(-) diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 8f621c19..c9eb760b 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -468,6 +468,7 @@ impl Application for Window { }; let mut header = header_bar() + .window_id(window::Id::MAIN) .title("COSMIC Design System - Iced") .on_close(Message::Close) .on_drag(Message::Drag) diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index b9b29c84..dd47212b 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -5,7 +5,7 @@ use cosmic::{ iced::{self, event, window}, iced_core::{id, Alignment, Length, Point}, iced_widget::{column, container, scrollable, text, text_input}, - widget::{button, cosmic_container}, + widget::{button, header_bar}, ApplicationExt, Command, }; @@ -97,6 +97,7 @@ impl cosmic::Application for MultiWindow { let (id, spawn_window) = window::spawn(window::Settings { position: Default::default(), exit_on_close_request: count % 2 == 0, + decorations: false, ..Default::default() }); @@ -140,13 +141,18 @@ impl cosmic::Application for MultiWindow { .align_items(Alignment::Center), ); - container(container(content).width(200).center_x()) + let window_content = container(container(content).width(200).center_x()) .style(cosmic::style::Container::Background) .width(Length::Fill) .height(Length::Fill) .center_x() - .center_y() - .into() + .center_y(); + + if id == window::Id::MAIN { + window_content.into() + } else { + column![header_bar().window_id(id), window_content].into() + } } fn view(&self) -> cosmic::prelude::Element { diff --git a/src/app/mod.rs b/src/app/mod.rs index 21dcf29a..9eee5274 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -647,6 +647,7 @@ impl ApplicationExt for App { .push_maybe(if core.window.show_headerbar { Some({ let mut header = crate::widget::header_bar() + .window_id(window::Id::MAIN) .title(&core.window.header_title) .on_drag(Message::Cosmic(cosmic::Message::Drag)) .on_close(Message::Cosmic(cosmic::Message::Close)); diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 603f5a73..34e2fef5 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -385,18 +385,12 @@ impl container::StyleSheet for Theme { } Container::HeaderBar => { let palette = self.cosmic(); - let mut header_top = palette.background.base; - let header_bottom = palette.background.base; - header_top.alpha = 0.8; + let header_top = palette.background.base; container::Appearance { icon_color: Some(Color::from(palette.accent.base)), text_color: Some(Color::from(palette.background.on)), - background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( - Linear::new(Radians(PI)) - .add_stop(0.0, header_top.into()) - .add_stop(1.0, header_bottom.into()), - ))), + background: Some(iced::Background::Color(header_top.into())), border_radius: BorderRadius::from([ palette.corner_radii.radius_xs[0], palette.corner_radii.radius_xs[3], diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index ff0e9925..7de4de28 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -4,8 +4,9 @@ use crate::{ext::CollectionWidget, widget, Element}; use apply::Apply; use derive_setters::Setters; -use iced::Length; -use std::borrow::Cow; +use iced::{window, Length}; +use iced_core::{renderer::Quad, widget::tree, Background, Color, Renderer, Widget}; +use std::{borrow::Cow, process::Child}; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -18,6 +19,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { start: Vec::new(), center: Vec::new(), end: Vec::new(), + window_id: None, } } @@ -43,6 +45,10 @@ pub struct HeaderBar<'a, Message> { #[setters(strip_option)] on_minimize: Option, + /// The window id for the headerbar. + #[setters(strip_option)] + window_id: Option, + /// Elements packed at the start of the headerbar. #[setters(skip)] start: Vec>, @@ -84,6 +90,194 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.end.push(widget.into()); self } + + /// Build the widget + #[must_use] + pub fn build(self) -> HeaderBarWidget<'a, Message> { + HeaderBarWidget { + window_id: self.window_id, + header_bar_inner: self.into_element(), + } + } +} + +pub struct HeaderBarWidget<'a, Message> { + header_bar_inner: Element<'a, Message>, + window_id: Option, +} + +impl<'a, Message: Clone + 'static> Widget + for HeaderBarWidget<'a, Message> +{ + fn children(&self) -> Vec { + vec![tree::Tree::new(&self.header_bar_inner)] + } + + fn width(&self) -> Length { + self.header_bar_inner.width() + } + + fn height(&self) -> Length { + self.header_bar_inner.height() + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn diff(&mut self, tree: &mut tree::Tree) { + tree.diff_children(&mut [&mut self.header_bar_inner]); + let prev = tree.state.downcast_mut::(); + if prev.window_id != self.window_id { + *prev = MyState::new(self.window_id); + } + } + + fn state(&self) -> tree::State { + let state = MyState::new(self.window_id); + tree::State::new(state) + } + + fn layout( + &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() + .layout(child_tree, renderer, limits); + iced_core::layout::Node::with_children(child.size(), vec![child]) + } + + fn draw( + &self, + tree: &tree::Tree, + renderer: &mut crate::Renderer, + theme: &::Theme, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + 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, + ); + + let state = tree.state.downcast_ref::(); + if !state.window_has_focus { + let header_bar_appearance = + ::appearance( + theme, + &crate::theme::Container::HeaderBar, + ); + let cosmic = theme.cosmic(); + let mut neutral_0 = cosmic.palette.neutral_0; + neutral_0.alpha = 0.3; + + // draw overlay rectangle + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border_radius: header_bar_appearance.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(neutral_0.into()), + ); + } + } + + fn on_event( + &mut self, + state: &mut tree::Tree, + 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 { + if let iced_core::Event::Window(id, iced_core::window::Event::Focused) = event { + let state = state.state.downcast_mut::(); + state.focus_window(id); + } else if let iced_core::Event::Window(id, iced_core::window::Event::Unfocused) = event { + let state = state.state.downcast_mut::(); + state.unfocus_window(id); + } + + let child_state = &mut state.children[0]; + let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget_mut().on_event( + child_state, + event, + child_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + state: &tree::Tree, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + 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, + ) + } + + fn operate( + &self, + state: &mut tree::Tree, + layout: iced_core::Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn iced_core::widget::Operation< + iced_core::widget::OperationOutputWrapper, + >, + ) { + 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); + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut tree::Tree, + layout: iced_core::Layout<'_>, + renderer: &crate::Renderer, + ) -> 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) + } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -207,6 +401,40 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - headerbar.into_element() + Element::new(headerbar.build()) + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self { + Element::new(headerbar) + } +} + +pub struct MyState { + pub window_id: Option, + pub window_has_focus: bool, +} + +impl MyState { + pub fn new(id: Option) -> Self { + Self { + window_id: id, + window_has_focus: id.is_none(), + } + } + + pub fn focus_window(&mut self, id: window::Id) { + if self.window_id != Some(id) { + return; + } + self.window_has_focus = true; + } + + pub fn unfocus_window(&mut self, id: window::Id) { + if self.window_id != Some(id) { + return; + } + self.window_has_focus = false; } } From 95bf466607f9bf8c8b6fd7e734b33172b1a9fec2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 11 Jan 2024 15:32:12 -0500 Subject: [PATCH 0062/1050] fix: scrollbar colors --- src/theme/style/iced.rs | 63 ++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 34e2fef5..7a307588 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -838,51 +838,74 @@ impl rule::StyleSheet for Theme { } } +#[derive(Default, Clone, Copy)] +pub enum Scrollable { + #[default] + Permanent, + Minimal, +} + /* * TODO: Scrollable */ impl scrollable::StyleSheet for Theme { - type Style = (); + type Style = Scrollable; - fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { + fn active(&self, style: &Self::Style) -> scrollable::Scrollbar { let cosmic = self.cosmic(); - scrollable::Scrollbar { - background: Some(Background::Color( - self.current_container().component.base.into(), - )), + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.7; + let mut a = scrollable::Scrollbar { + background: None, border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { - color: self.current_container().component.divider.into(), + color: neutral_5.into(), border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, + }; + + if matches!(style, Scrollable::Permanent) { + let mut neutral_3 = cosmic.palette.neutral_3; + neutral_3.alpha = 0.7; + a.background = Some(Background::Color(neutral_3.into())); } + + a } - fn hovered( - &self, - _style: &Self::Style, - _is_mouse_over_scrollbar: bool, - ) -> scrollable::Scrollbar { - let theme = self.cosmic(); + fn hovered(&self, style: &Self::Style, is_mouse_over_scrollbar: bool) -> scrollable::Scrollbar { + let cosmic = self.cosmic(); + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.7; - scrollable::Scrollbar { - background: Some(Background::Color( - self.current_container().component.hover.into(), - )), - border_radius: theme.corner_radii.radius_s.into(), + if is_mouse_over_scrollbar { + let mut hover_overlay = cosmic.palette.neutral_0; + hover_overlay.alpha = 0.2; + neutral_5 = over(hover_overlay, neutral_5); + } + let mut a = scrollable::Scrollbar { + background: None, + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, scroller: scrollable::Scroller { - color: theme.accent.base.into(), - border_radius: theme.corner_radii.radius_s.into(), + color: neutral_5.into(), + border_radius: cosmic.corner_radii.radius_s.into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, + }; + if matches!(style, Scrollable::Permanent) { + let mut neutral_3 = cosmic.palette.neutral_3; + neutral_3.alpha = 0.7; + a.background = Some(Background::Color(neutral_3.into())); } + + a } } From 2c6db80c64a536d64fb47e1f36e297f48e7e579f Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 09:39:03 -0700 Subject: [PATCH 0063/1050] cosmic-config-derive: use transaction for write_entry --- cosmic-config-derive/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 69fd6e96..b8b2fb09 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -45,7 +45,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let write_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; quote! { - cosmic_config::ConfigSet::set(config, stringify!(#field_name), &self.#field_name)?; + cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; } }); From 63802dfcf9c1e30cc2a72c15953af4c4af9f8eef Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 09:39:35 -0700 Subject: [PATCH 0064/1050] cosmic-config-derive: automatically generate setters --- cosmic-config-derive/src/lib.rs | 25 +++++++++++++++++++++++++ cosmic-theme/src/model/mode.rs | 5 ----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index b8b2fb09..79dad713 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -80,6 +80,27 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { } }); + let setters = fields.iter().filter_map(|field| { + let field_name = &field.ident.as_ref()?; + let field_type = &field.ty; + let setter_name = quote::format_ident!("set_{}", field_name); + let doc = format!("Sets [`{name}::{field_name}`] and writes to [`cosmic_config::Config`] if changed"); + Some(quote! { + #[doc = #doc] + /// + /// Returns `Ok(true)` when the field's value has changed and was written to disk + pub fn #setter_name(&mut self, config: &cosmic_config::Config, value: #field_type) -> Result { + if self.#field_name != value { + self.#field_name = value; + cosmic_config::ConfigSet::set(config, stringify!(#field_name), &self.#field_name)?; + Ok(true) + } else { + Ok(false) + } + } + }) + }); + let gen = quote! { impl CosmicConfigEntry for #name { const VERSION: u64 = #version; @@ -115,6 +136,10 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { (errors, keys) } } + + impl #name { + #(#setters)* + } }; gen.into() diff --git a/cosmic-theme/src/model/mode.rs b/cosmic-theme/src/model/mode.rs index d9a9b14d..b2ad99f9 100644 --- a/cosmic-theme/src/model/mode.rs +++ b/cosmic-theme/src/model/mode.rs @@ -35,11 +35,6 @@ impl ThemeMode { 1 } - /// Set auto-switch from light to dark mode - pub fn set_auto_switch(config: &Config, value: bool) -> Result<(), cosmic_config::Error> { - config.set("auto_switch", value) - } - /// Get the config for the theme mode pub fn config() -> Result { Config::new(THEME_MODE_ID, Self::version()) From 94a1bbdaa5315aa42cf9d5a48be1410968a6e326 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 09:40:49 -0700 Subject: [PATCH 0065/1050] cosmic-config: pretty print config file data --- cosmic-config/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index e7c7f5b2..6a0d8b67 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -314,7 +314,7 @@ impl<'a> ConfigSet for ConfigTransaction<'a> { fn set(&self, key: &str, value: T) -> Result<(), Error> { //TODO: sanitize key (no slashes, cannot be . or ..) let key_path = self.config.key_path(key)?; - let data = ron::to_string(&value)?; + let data = ron::ser::to_string_pretty(&value, ron::ser::PrettyConfig::new())?; //TODO: replace duplicates? { let mut updates = self.updates.lock().unwrap(); From 5081440c41a477df580ae63ff37743b85d29c448 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 10:06:25 -0700 Subject: [PATCH 0066/1050] Generate documentation and publish to github pages --- .github/workflows/pages.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..8ef929e6 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,21 @@ +name: Pages + +on: + push: + branches: + - master + +jobs: + pages: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build documentation + run: cargo doc --verbose + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc From 2137b9ebeb0c36a83249b40561d8b800669ae67c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 10:14:35 -0700 Subject: [PATCH 0067/1050] Checkout submodules in pages CI --- .github/workflows/pages.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8ef929e6..f45975be 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,7 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive - name: Build documentation run: cargo doc --verbose - name: Deploy documentation From 80293595612ffab8f16996b47958b952c8b2d100 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 10:17:29 -0700 Subject: [PATCH 0068/1050] Update iced to fix docs error --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 611ce160..b33248a5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 611ce160e6b2ccab388fc8e303d4795a3365afbc +Subproject commit b33248a5cc54bc708baa3e128e4f0325bdff066e From 250c6f769cc0f0674308720c592c410c7ec77e0e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 12 Jan 2024 10:26:08 -0700 Subject: [PATCH 0069/1050] Set default docs features --- .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 f45975be..f61444d4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -16,7 +16,7 @@ jobs: with: submodules: recursive - name: Build documentation - run: cargo doc --verbose + run: cargo doc --verbose --features tokio,winit,wgpu - name: Deploy documentation uses: peaceiris/actions-gh-pages@v3 with: From 6a952e9350f3c10d21eb2ccbe0d206ff0498a069 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 17 Jan 2024 11:43:08 -0500 Subject: [PATCH 0070/1050] chore: update cosmic-protocols --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bca811bc..7c7b0b01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.7.3" tokio = { version = "1.24.2", optional = true } -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c1b6516", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e", optional = true } slotmap = "1.0.6" fraction = "0.14.0" cosmic-config = { path = "cosmic-config" } From 218a2e071c60da9471c8d572aa5592299cf7deee Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 17 Jan 2024 13:33:39 -0700 Subject: [PATCH 0071/1050] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index b33248a5..64ed230b 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b33248a5cc54bc708baa3e128e4f0325bdff066e +Subproject commit 64ed230b727fa3613a5f7e76d9668c082f8e1247 From 4e18199444aecbc60f25a12e8adb91926aa5e653 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 17 Jan 2024 13:37:30 -0700 Subject: [PATCH 0072/1050] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 64ed230b..6115280d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 64ed230b727fa3613a5f7e76d9668c082f8e1247 +Subproject commit 6115280d5277c50b8539d4eff6ab61050c51b592 From 994e93d6d2f90f947d56376094eb19877d708063 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 17 Jan 2024 16:48:33 -0500 Subject: [PATCH 0073/1050] fix: better handling of secure inputs --- examples/cosmic/src/window/demo.rs | 10 +- src/app/cosmic.rs | 2 +- src/widget/text_input/input.rs | 307 +++++++++++++++++------------ 3 files changed, 187 insertions(+), 132 deletions(-) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 4efe5afa..8deea5b0 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -89,6 +89,7 @@ pub enum Message { ClearAll, CardsToggled(bool), ColorPickerUpdate(ColorPickerUpdate), + Hidden, } pub enum Output { @@ -115,6 +116,7 @@ pub struct State { cards: Vec, pub timeline: Rc>, pub color_picker_model: ColorPickerModel, + pub hidden: bool, } impl Default for State { @@ -162,6 +164,7 @@ impl Default for State { ], timeline: Rc::new(RefCell::new(Default::default())), color_picker_model: ColorPickerModel::new("Hex", "RGB", None, None), + hidden: false, } } } @@ -210,6 +213,9 @@ impl State { Message::ColorPickerUpdate(u) => { _ = self.color_picker_model.update::(u); } + Message::Hidden => { + self.hidden = !self.hidden; + } } None @@ -470,9 +476,11 @@ impl State { .padding(16) .style(cosmic::theme::Container::Background) .into(), - text_input( + cosmic::widget::text_input::secure_input( "Type to search apps or type “?” for more options...", &self.entry_value, + Some(Message::Hidden), + self.hidden, ) .on_input(Message::InputChanged) .size(20) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index e06840ca..45ebed2c 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -228,7 +228,7 @@ where .core() .single_instance .then(|| super::single_instance_subscription::()) - .unwrap_or_else(|| Subscription::none()), + .unwrap_or_else(Subscription::none), ]) } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 04215d76..209c29e7 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -476,7 +476,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::new(self.is_secure)) } fn diff(&mut self, tree: &mut Tree) { @@ -489,6 +489,12 @@ where state.is_pasting = None; state.dragging_state = None; } + + if state.is_secure != self.is_secure { + state.is_secure = self.is_secure; + state.dirty = true; + } + let mut children: Vec<_> = self .leading_icon .iter_mut() @@ -522,13 +528,13 @@ where ) -> layout::Node { let font = self.font.unwrap_or_else(|| renderer.default_font()); if self.dnd_icon { + let state = tree.state.downcast_mut::(); let limits = limits.width(Length::Shrink).height(Length::Shrink); let size = self.size.unwrap_or_else(|| renderer.default_size().0); let bounds = limits.max(); - let state = tree.state.downcast_mut::(); let value_paragraph = &mut state.value; let v = self.value.to_string(); value_paragraph.update(Text { @@ -551,7 +557,7 @@ where let size = limits.resolve(Size::new(width, height)); layout::Node::with_children(size, vec![layout::Node::new(size)]) } else { - layout( + let res = layout( renderer, limits, self.width, @@ -566,7 +572,29 @@ where self.helper_line_height, font, tree, - ) + ); + + // XXX not ideal, but we need to update the cache when is_secure changes + let size = self.size.unwrap_or_else(|| renderer.default_size().0); + let line_height = self.line_height; + let state = tree.state.downcast_mut::(); + if state.dirty { + state.dirty = false; + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + replace_paragraph( + state, + Layout::new(&res), + &value, + font, + iced::Pixels(size), + line_height, + ); + } + res } } @@ -630,6 +658,10 @@ where ) -> event::Status { let text_layout = self.text_layout(layout); let mut index = 0; + let font = self.font.unwrap_or_else(|| renderer.default_font()); + let size = self.size.unwrap_or_else(|| renderer.default_size().0); + let line_height = self.line_height; + if let (Some(leading_icon), Some(tree)) = (self.leading_icon.as_mut(), tree.children.get_mut(index)) { @@ -637,22 +669,13 @@ where children.next(); let leading_icon_layout = children.next().unwrap(); - if cursor_position.is_over(leading_icon_layout.bounds()) { - return leading_icon.as_widget_mut().on_event( - tree, - event.clone(), - leading_icon_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - } else if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft) - ) { - leading_icon.as_widget_mut().on_event( + if cursor_position.is_over(leading_icon_layout.bounds()) + || matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft) + ) + { + let res = leading_icon.as_widget_mut().on_event( tree, event.clone(), leading_icon_layout, @@ -662,6 +685,9 @@ where shell, viewport, ); + if res == event::Status::Captured { + return res; + } } index += 1; } @@ -675,22 +701,13 @@ where } let trailing_icon_layout = children.next().unwrap(); - if cursor_position.is_over(trailing_icon_layout.bounds()) { - return trailing_icon.as_widget_mut().on_event( - tree, - event.clone(), - trailing_icon_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); - } else if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft) - ) { - trailing_icon.as_widget_mut().on_event( + if cursor_position.is_over(trailing_icon_layout.bounds()) + | matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorLeft) + ) + { + let res = trailing_icon.as_widget_mut().on_event( tree, event.clone(), trailing_icon_layout, @@ -700,6 +717,9 @@ where shell, viewport, ); + if res == event::Status::Captured { + return res; + } } } @@ -707,12 +727,11 @@ where event, text_layout.children().next().unwrap(), cursor_position, - renderer, clipboard, shell, &mut self.value, - self.size, - self.font, + size, + font, self.is_secure, self.on_input.as_deref(), self.on_paste.as_deref(), @@ -722,7 +741,7 @@ where self.dnd_icon, self.on_dnd_command_produced.as_deref(), self.surface_ids, - self.line_height, + line_height, layout, ) } @@ -1062,12 +1081,11 @@ pub fn update<'a, Message>( event: Event, text_layout: Layout<'_>, cursor_position: mouse::Cursor, - renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, value: &mut Value, - size: Option, - font: Option<::Font>, + size: f32, + font: ::Font, is_secure: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, @@ -1083,12 +1101,18 @@ pub fn update<'a, Message>( where Message: Clone, { - let font = font.unwrap_or_else(|| renderer.default_font()); - let size = size.unwrap_or_else(|| renderer.default_size().0); let update_cache = |state, value| { replace_paragraph(state, layout, value, font, iced::Pixels(size), line_height); }; + let mut secured_value = if is_secure { + value.secure() + } else { + value.clone() + }; + let unsecured_value = value; + let value = &mut secured_value; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -1125,9 +1149,6 @@ where // if something is already selected, we can start a drag and drop for a // single click that is on top of the selected text // is the click on selected text? - if is_secure { - return event::Status::Ignored; - } if let ( Some(on_start_dnd), @@ -1164,17 +1185,23 @@ where }; if cursor_position.is_over(selection_bounds) { + // XXX never start a dnd if the input is secure + if is_secure { + return event::Status::Ignored; + } let text = state.selected_text(&value.to_string()).unwrap_or_default(); state.dragging_state = Some(DraggingState::Dnd(DndAction::empty(), text.clone())); - let mut editor = Editor::new(value, &mut state.cursor); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.delete(); - let message = (on_input)(editor.contents()); + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); + let message = (on_input)(contents); shell.publish(message); shell.publish(on_start_dnd(state.clone())); - let state = state.clone(); + let state_clone = state.clone(); shell.publish(on_dnd_command_produced(Box::new(move || { platform_specific::wayland::data_device::ActionInner::StartDnd { mime_types: SUPPORTED_TEXT_MIME_TYPES @@ -1185,26 +1212,18 @@ where origin_id: window_id, icon_id: Some(DndIcon::Widget( icon_id, - Box::new(state.clone()), + Box::new(state_clone.clone()), )), data: Box::new(TextInputString(text.clone())), } }))); + + update_cache(state, &unsecured_value); } else { + update_cache(state, value); // existing logic for setting the selection let position = if target > 0.0 { - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - find_cursor_position( - text_layout.bounds(), - &value, - state, - target, - ) + find_cursor_position(text_layout.bounds(), value, state, target) } else { None }; @@ -1219,13 +1238,8 @@ where (None, click::Kind::Single, _) => { // existing logic for setting the selection let position = if target > 0.0 { - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - find_cursor_position(text_layout.bounds(), &value, state, target) + update_cache(state, value); + find_cursor_position(text_layout.bounds(), value, state, target) } else { None }; @@ -1234,6 +1248,8 @@ where state.dragging_state = Some(DraggingState::Selection); } (None | Some(DraggingState::Selection), click::Kind::Double, _) => { + update_cache(state, value); + if is_secure { state.cursor.select_all(value); } else { @@ -1249,6 +1265,7 @@ where state.dragging_state = Some(DraggingState::Selection); } (None | Some(DraggingState::Selection), click::Kind::Triple, _) => { + update_cache(state, value); state.cursor.select_all(value); state.dragging_state = Some(DraggingState::Selection); } @@ -1274,17 +1291,13 @@ where if matches!(state.dragging_state, Some(DraggingState::Selection)) { let target = position.x - text_layout.bounds().x; - let value: Value = if is_secure { - value.secure() - } else { - value.clone() - }; + update_cache(state, value); let position = - find_cursor_position(text_layout.bounds(), &value, state, target).unwrap_or(0); + find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0); state .cursor - .select_range(state.cursor.start(&value), position); + .select_range(state.cursor.start(value), position); return event::Status::Captured; } @@ -1301,16 +1314,22 @@ where && !state.keyboard_modifiers.command() && !c.is_control() { - let mut editor = Editor::new(value, &mut state.cursor); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.insert(c); - - let message = (on_input)(editor.contents()); + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); + let message = (on_input)(contents); shell.publish(message); focus.updated_at = Instant::now(); - update_cache(state, value); + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; + update_cache(state, &value); return event::Status::Captured; } @@ -1345,33 +1364,46 @@ where } } - let mut editor = Editor::new(value, &mut state.cursor); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.backspace(); + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); let message = (on_input)(editor.contents()); shell.publish(message); - update_cache(state, value); + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; + update_cache(state, &value); } keyboard::KeyCode::Delete => { if platform::is_jump_modifier_pressed(modifiers) && state.cursor.selection(value).is_none() { if is_secure { - let cursor_pos = state.cursor.end(value); - state.cursor.select_range(cursor_pos, value.len()); + let cursor_pos = state.cursor.end(unsecured_value); + state.cursor.select_range(cursor_pos, unsecured_value.len()); } else { - state.cursor.select_right_by_words(value); + state.cursor.select_right_by_words(unsecured_value); } } - let mut editor = Editor::new(value, &mut state.cursor); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.delete(); - - let message = (on_input)(editor.contents()); + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); + let message = (on_input)(contents); shell.publish(message); + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; - update_cache(state, value); + update_cache(state, &value); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) && !is_secure { @@ -1416,22 +1448,27 @@ where } } keyboard::KeyCode::C if state.keyboard_modifiers.command() => { - if let Some((start, end)) = state.cursor.selection(value) { - clipboard.write(value.select(start, end).to_string()); + if !is_secure { + if let Some((start, end)) = state.cursor.selection(value) { + clipboard.write(value.select(start, end).to_string()); + } } } + // XXX if we want to allow cutting of secure text, we need to + // update the cache and decide which value to cut keyboard::KeyCode::X if state.keyboard_modifiers.command() => { - if let Some((start, end)) = state.cursor.selection(value) { - clipboard.write(value.select(start, end).to_string()); + if !is_secure { + if let Some((start, end)) = state.cursor.selection(value) { + clipboard.write(value.select(start, end).to_string()); + } + + let mut editor = Editor::new(value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + + shell.publish(message); } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); } keyboard::KeyCode::V => { if state.keyboard_modifiers.command() { @@ -1448,20 +1485,28 @@ where Value::new(&content) }; - let mut editor = Editor::new(value, &mut state.cursor); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.paste(content.clone()); + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); let message = if let Some(paste) = &on_paste { - (paste)(editor.contents()) + (paste)(contents) } else { - (on_input)(editor.contents()) + (on_input)(contents) }; shell.publish(message); state.is_pasting = Some(content); - update_cache(state, value); + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; + + update_cache(state, &value); } else { state.is_pasting = None; } @@ -1582,13 +1627,8 @@ where state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None); // existing logic for setting the selection let position = if target > 0.0 { - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - find_cursor_position(text_layout.bounds(), &value, state, target) + update_cache(state, value); + find_cursor_position(text_layout.bounds(), value, state, target) } else { None }; @@ -1652,13 +1692,8 @@ where let target = x as f32 - text_layout.bounds().x; // existing logic for setting the selection let position = if target > 0.0 { - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - find_cursor_position(text_layout.bounds(), &value, state, target) + update_cache(state, value); + find_cursor_position(text_layout.bounds(), value, state, target) } else { None }; @@ -1728,21 +1763,26 @@ where return event::Status::Captured; }; - let mut editor = Editor::new(value, &mut state.cursor); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.paste(Value::new(content.as_str())); + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); + if let Some(on_paste) = on_paste.as_ref() { - let message = (on_paste)(editor.contents()); - shell.publish(message); - } - if let Some(on_paste) = on_paste { - let message = (on_paste)(editor.contents()); + let message = (on_paste)(contents); shell.publish(message); } shell.publish(on_dnd_command_produced(Box::new(move || { platform_specific::wayland::data_device::ActionInner::DndFinished }))); + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; + update_cache(state, &value); return event::Status::Captured; } return event::Status::Ignored; @@ -2159,6 +2199,8 @@ pub struct State { pub value: crate::Paragraph, pub placeholder: crate::Paragraph, pub label: crate::Paragraph, + pub dirty: bool, + pub is_secure: bool, is_focused: Option, dragging_state: Option, #[cfg(feature = "wayland")] @@ -2178,8 +2220,11 @@ struct Focus { impl State { /// Creates a new [`State`], representing an unfocused [`TextInput`]. - pub fn new() -> Self { - Self::default() + pub fn new(is_secure: bool) -> Self { + Self { + is_secure, + ..Self::default() + } } /// Returns the current value of the selected text in the [`TextInput`]. @@ -2207,8 +2252,9 @@ impl State { } /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused() -> Self { + pub fn focused(is_secure: bool) -> Self { Self { + is_secure, value: crate::Paragraph::new(), placeholder: crate::Paragraph::new(), label: crate::Paragraph::new(), @@ -2221,6 +2267,7 @@ impl State { last_click: None, cursor: Cursor::default(), keyboard_modifiers: keyboard::Modifiers::default(), + dirty: false, } } From 3eed30f72375c9496cc20108a5a7836cd6d3822b Mon Sep 17 00:00:00 2001 From: Dominic Gerhauser Date: Thu, 18 Jan 2024 16:00:04 +0100 Subject: [PATCH 0074/1050] fix: update id in applet example --- examples/applet/src/window.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 7a672559..45cd165f 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -59,13 +59,12 @@ impl cosmic::Application for Window { return if let Some(p) = self.popup.take() { destroy_popup(p) } else { - self.id_ctr += 1; - let new_id = Id(self.id_ctr); + let new_id = Id::unique(); self.popup.replace(new_id); let mut popup_settings = self.core .applet - .get_popup_settings(Id(0), new_id, None, None, None); + .get_popup_settings(Id::MAIN, new_id, None, None, None); popup_settings.positioner.size_limits = Limits::NONE .max_width(372.0) .min_width(300.0) From efe4ce2f5b514e4d553ab82c0c873dca7585c028 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 18 Jan 2024 19:01:11 -0500 Subject: [PATCH 0075/1050] refactor: config improvements --- cosmic-config/src/dbus.rs | 11 ++----- cosmic-config/src/lib.rs | 12 ++++++-- cosmic-config/src/subscription.rs | 35 ++++++++++++++-------- examples/applet/src/window.rs | 1 - src/app/core.rs | 34 +++++++++++++-------- src/app/cosmic.rs | 49 +++++-------------------------- src/theme/mod.rs | 13 ++++---- 7 files changed, 69 insertions(+), 86 deletions(-) diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index a2c4f5af..4977672b 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,6 +1,6 @@ use std::ops::Deref; -use crate::CosmicConfigEntry; +use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::futures::{future::pending, StreamExt}; @@ -51,13 +51,6 @@ impl Watcher { } } -#[derive(Debug)] -pub struct Update { - pub errors: Vec, - pub keys: Vec<&'static str>, - pub config: T, -} - pub fn watcher_subscription( settings_daemon: CosmicSettingsDaemonProxy<'static>, config_id: &'static str, @@ -79,7 +72,7 @@ pub fn watcher_subscription config, Err((errors, default)) => { if !errors.is_empty() { - eprintln!("Failed to get config: {errors:?}"); + eprintln!("Error getting config: {config_id} {errors:?}"); } default } diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 6a0d8b67..d6a4072b 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -32,6 +32,7 @@ pub enum Error { Notify(notify::Error), Ron(ron::Error), RonSpanned(ron::error::SpannedError), + GetKey(String, std::io::Error), } impl fmt::Display for Error { @@ -44,6 +45,7 @@ impl fmt::Display for Error { Self::Notify(err) => err.fmt(f), Self::Ron(err) => err.fmt(f), Self::RonSpanned(err) => err.fmt(f), + Self::GetKey(key, err) => write!(f, "failed to get key '{}': {}", key, err), } } } @@ -264,11 +266,11 @@ impl ConfigGet for Config { let key_path = self.key_path(key)?; let data = if key_path.is_file() { // Load user override - fs::read_to_string(key_path)? + fs::read_to_string(key_path).map_err(|err| Error::GetKey(key.to_string(), err))? } else { // Load system default let default_path = self.default_path(key)?; - fs::read_to_string(default_path)? + fs::read_to_string(default_path).map_err(|err| Error::GetKey(key.to_string(), err))? }; let t = ron::from_str(&data)?; Ok(t) @@ -339,3 +341,9 @@ where changed_keys: &[T], ) -> (Vec, Vec<&'static str>); } + +pub struct Update { + pub errors: Vec, + pub keys: Vec<&'static str>, + pub config: T, +} diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 8c4f3c6d..e1e4f7ac 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -12,8 +12,7 @@ pub enum ConfigState { } pub enum ConfigUpdate { - Update(T), - UpdateError(T, Vec), + Update(crate::Update), Failed, } @@ -24,7 +23,7 @@ pub fn config_subscription< id: I, config_id: Cow<'static, str>, config_version: u64, -) -> iced_futures::Subscription<(I, Result, T)>)> { +) -> iced_futures::Subscription> { subscription::channel(id, 100, move |mut output| { let config_id = config_id.clone(); async move { @@ -45,7 +44,7 @@ pub fn config_state_subscription< id: I, config_id: Cow<'static, str>, config_version: u64, -) -> iced_futures::Subscription<(I, Result, T)>)> { +) -> iced_futures::Subscription> { subscription::channel(id, 100, move |mut output| { let config_id = config_id.clone(); async move { @@ -64,7 +63,7 @@ async fn start_listening< T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, >( state: ConfigState, - output: &mut mpsc::Sender<(I, Result, T)>)>, + output: &mut mpsc::Sender>, id: I, ) -> ConfigState { use iced_futures::futures::{future::pending, StreamExt}; @@ -90,11 +89,21 @@ async fn start_listening< match T::get_entry(&config) { Ok(t) => { - _ = output.send((id, Ok(t.clone()))).await; + let update = crate::Update { + errors: Vec::new(), + keys: Vec::new(), + config: t.clone(), + }; + _ = output.send(update).await; ConfigState::Waiting(t, watcher, rx, config) } Err((errors, t)) => { - _ = output.send((id, Err((errors, t.clone())))).await; + let update = crate::Update { + errors: errors, + keys: Vec::new(), + config: t.clone(), + }; + _ = output.send(update).await; ConfigState::Waiting(t, watcher, rx, config) } } @@ -104,11 +113,13 @@ async fn start_listening< let (errors, changed) = conf_data.update_keys(&config, &keys); if !changed.is_empty() { - if errors.is_empty() { - _ = output.send((id, Ok(conf_data.clone()))).await; - } else { - _ = output.send((id, Err((errors, conf_data.clone())))).await; - } + _ = output + .send(crate::Update { + errors: errors, + keys: changed, + config: conf_data.clone(), + }) + .await; } ConfigState::Waiting(conf_data, watcher, rx, config) } diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 45cd165f..c1706c65 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -13,7 +13,6 @@ const ID: &str = "com.system76.CosmicAppletExample"; pub struct Window { core: Core, popup: Option, - id_ctr: u128, example_row: bool, } diff --git a/src/app/core.rs b/src/app/core.rs index 764063d8..3ae740b6 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -214,27 +214,37 @@ impl Core { self.system_theme_mode } - #[cfg(feature = "dbus-config")] - pub fn watch_config( + pub fn watch_config< + T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq, + >( &self, config_id: &'static str, - ) -> iced::Subscription> { + ) -> iced::Subscription> { + #[cfg(feature = "dbus-config")] if let Some(settings_daemon) = self.settings_daemon.clone() { - cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false) - } else { - iced::Subscription::none() + return cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false); } + cosmic_config::config_subscription( + std::any::TypeId::of::(), + std::borrow::Cow::Borrowed(config_id), + T::VERSION, + ) } - #[cfg(feature = "dbus-config")] - pub fn watch_state( + pub fn watch_state< + T: CosmicConfigEntry + Send + Sync + Default + 'static + Clone + PartialEq, + >( &self, state_id: &'static str, - ) -> iced::Subscription> { + ) -> iced::Subscription> { + #[cfg(feature = "dbus-config")] if let Some(settings_daemon) = self.settings_daemon.clone() { - cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true) - } else { - iced::Subscription::none() + return cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true); } + cosmic_config::config_subscription( + std::any::TypeId::of::(), + std::borrow::Cow::Borrowed(state_id), + T::VERSION, + ) } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 45ebed2c..471db012 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -22,6 +22,7 @@ use iced::window; #[cfg(not(any(feature = "multi-window", feature = "wayland")))] use iced::Application as IcedApplication; use iced_futures::event::listen_raw; +use iced_futures::futures::executor::block_on; #[cfg(not(feature = "wayland"))] use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] @@ -66,9 +67,6 @@ pub enum Message { WmCapabilities(window::Id, WindowManagerCapabilities), /// Activate the application Activate(String), - #[cfg(feature = "dbus-config")] - /// dbus settings daemon setup - SettingsDaemon(zbus::Result>), } #[derive(Default)] @@ -85,16 +83,14 @@ where type Message = super::Message; type Theme = Theme; - fn new((core, flags): Self::Flags) -> (Self, iced::Command) { + fn new((mut core, flags): Self::Flags) -> (Self, iced::Command) { + #[cfg(feature = "dbus-config")] + { + core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); + } + let (model, command) = T::init(core, flags); - #[cfg(feature = "dbus-config")] - let command = iced::Command::batch(vec![ - command, - iced::Command::perform(cosmic_config::dbus::settings_daemon_proxy(), |p| { - super::Message::Cosmic(super::cosmic::Message::SettingsDaemon(p)) - }), - ]); (Self::new(model), command) } @@ -176,7 +172,6 @@ where keyboard_nav::subscription() .map(Message::KeyboardNav) .map(super::Message::Cosmic), - #[cfg(feature = "dbus-config")] self.app .core() .watch_config::(if self.app.core().system_theme_mode.is_dark { @@ -191,27 +186,6 @@ where Message::SystemThemeChange(crate::theme::Theme::system(Arc::new(update.config))) }) .map(super::Message::Cosmic), - #[cfg(not(feature = "dbus-config"))] - theme::subscription(self.app.core().system_theme_mode.is_dark) - .map(Message::SystemThemeChange) - .map(super::Message::Cosmic), - #[cfg(not(feature = "dbus-config"))] - cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>( - 0, - cosmic_theme::THEME_MODE_ID.into(), - cosmic_theme::ThemeMode::version(), - ) - .map(|(_, u)| match u { - Ok(t) => Message::SystemThemeModeChange(t), - Err((errors, t)) => { - for e in errors { - tracing::error!("{e}"); - } - Message::SystemThemeModeChange(t) - } - }) - .map(super::Message::Cosmic), - #[cfg(feature = "dbus-config")] self.app .core() .watch_config::(cosmic_theme::THEME_MODE_ID) @@ -422,15 +396,6 @@ impl Cosmic { _token, ); } - #[cfg(feature = "dbus-config")] - Message::SettingsDaemon(p) => match p { - Ok(p) => { - self.app.core_mut().settings_daemon = Some(p); - } - Err(e) => { - tracing::error!("Failed to connect to settings daemon: {e}"); - } - }, } iced::Command::none() diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 42a344ac..ebc90d99 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -85,15 +85,12 @@ pub fn subscription(is_dark: bool) -> Subscription { .into(), crate::cosmic_theme::Theme::version(), ) - .map(|(_, res)| { - let theme = res.unwrap_or_else(|(errors, theme)| { - for err in errors { - tracing::error!("{:?}", err); - } - theme - }); + .map(|res| { + for err in res.errors { + tracing::error!("{:?}", err); + } - Theme::system(Arc::new(theme)) + Theme::system(Arc::new(res.config)) }) } From 6f5e1b5baad53d4d8194d243cb76064477a95ec1 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sun, 21 Jan 2024 12:26:01 -0700 Subject: [PATCH 0076/1050] search widget: fix submit message --- src/widget/search/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/search/mod.rs b/src/widget/search/mod.rs index 4a1f374e..b7f1c794 100644 --- a/src/widget/search/mod.rs +++ b/src/widget/search/mod.rs @@ -73,7 +73,7 @@ pub fn search(model: &Model, on_emit: fn(Message) -> M) -> c &model.phrase, Message::Changed, Message::Clear, - Some(Message::Clear), + Some(Message::Submit), ) .into(), From 25eea464b96e02c254e361b5a8cab252ddd2eddb Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 22 Jan 2024 14:06:41 +0100 Subject: [PATCH 0077/1050] feat(segmented_button): close tab on middle click --- src/widget/segmented_button/widget.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 90844419..fa04a464 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -13,7 +13,7 @@ use iced::{ use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; use iced_core::widget::{self, operation, tree}; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; -use iced_core::{BorderRadius, Point, Renderer as IcedRenderer, Text}; +use iced_core::{Point, Renderer as IcedRenderer, Text}; use slotmap::SecondaryMap; use std::marker::PhantomData; @@ -70,6 +70,7 @@ pub trait SegmentedVariant { /// A conjoined group of items that function together as a button. #[derive(Setters)] +#[must_use] pub struct SegmentedButton<'a, Variant, SelectionMode, Message> where Model: Selectable, @@ -380,6 +381,7 @@ where // 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 cursor_position.is_over(close_bounds( bounds, @@ -395,6 +397,15 @@ where return event::Status::Captured; } } + + // Emit close message if the tab is middle clicked. + if let Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Middle, + )) = event + { + shell.publish(on_close(key)); + return event::Status::Captured; + } } } From bb8be4e3d5b080f71c0672c6ae729c02118f4e97 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 22 Jan 2024 15:57:59 +0100 Subject: [PATCH 0078/1050] feat(segmented_button): use scroll wheel to switch between tabs --- src/widget/segmented_button/widget.rs | 57 ++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index fa04a464..7e8c6973 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -10,12 +10,14 @@ use iced::{ alignment, event, keyboard, mouse, touch, Background, Color, Command, Event, Length, Rectangle, Size, }; +use iced_core::mouse::ScrollDelta; use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping}; use iced_core::widget::{self, operation, tree}; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; use iced_core::{Point, Renderer as IcedRenderer, Text}; -use slotmap::SecondaryMap; +use slotmap::{Key, SecondaryMap}; use std::marker::PhantomData; +use std::time::{Duration, Instant}; /// State that is maintained by each individual widget. #[derive(Default)] @@ -30,6 +32,8 @@ pub struct LocalState { hovered: Entity, /// The paragraphs for each text. paragraphs: SecondaryMap, + /// Time since last tab activation from wheel movements. + wheel_timestamp: Option, } impl operation::Focusable for LocalState { @@ -422,6 +426,57 @@ where break; } } + + if let Some(on_activate) = self.on_activate.as_ref() { + if 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.map_or(true, |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; + + 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); + } + + 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; + } + } + break; + } + } + } + + if let Some(key) = activate_key { + shell.publish(on_activate(key)); + return event::Status::Captured; + } + } + } + } + } + } } else { state.hovered = Entity::default(); } From 507c4c97bbe5397c21ccdfb445b2e2f4aea4db22 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 22 Jan 2024 08:47:48 +0100 Subject: [PATCH 0079/1050] chore(widget): remove cosmic-settings navigation widgets --- src/widget/mod.rs | 2 -- src/widget/navigation.rs | 56 ---------------------------------------- 2 files changed, 58 deletions(-) delete mode 100644 src/widget/navigation.rs diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 448bf2ea..3684b954 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -134,8 +134,6 @@ pub use nav_bar::nav_bar; pub mod nav_bar_toggle; pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle}; -pub mod navigation; - pub mod popover; pub use popover::{popover, Popover}; diff --git a/src/widget/navigation.rs b/src/widget/navigation.rs deleted file mode 100644 index 0fd4c7b8..00000000 --- a/src/widget/navigation.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::widget::{button, column, container, horizontal_space, icon, list, row, text}; -use crate::{theme, Apply, Element}; -use iced::Length; - -#[must_use] -pub fn page_list_item<'a, Message: 'static + Clone>( - title: &'a str, - description: &'a str, - icon: &'a str, - message: Message, -) -> Element<'a, Message> { - let control = row::with_children(vec![ - horizontal_space(Length::Fill).into(), - icon::from_name("go-next-symbolic").size(16).into(), - ]); - - super::settings::item::builder(title) - .description(description) - .icon(icon::from_name(icon).size(16)) - .control(control) - .spacing(16) - .apply(container) - .padding([20, 24]) - .style(theme::Container::custom(list::style)) - .apply(button) - .style(theme::Button::Transparent) - .on_press(message) - .into() -} - -#[must_use] -pub fn sub_page_header<'a, Message: 'static + Clone>( - sub_page: &'a str, - parent_page: &'a str, - on_press: Message, -) -> Element<'a, Message> { - let previous_button = button::icon(icon::from_name("go-previous-symbolic")) - .extra_small() - .label(parent_page) - .spacing(16) - .style(button::Style::Link) - .on_press(on_press); - - let sub_page_header = row::with_capacity(2) - .push(text::title3(sub_page)) - .push(horizontal_space(Length::Fill)); - - column::with_capacity(2) - .push(previous_button) - .push(sub_page_header) - .spacing(6) - .into() -} From b09b3db81a7a22171ce4d9bb4948ce57007cef3d Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 22 Jan 2024 08:48:45 +0100 Subject: [PATCH 0080/1050] chore(widget): remove redundant `search` widget The `text_input` widget's search variant replaced this. --- src/widget/mod.rs | 2 - src/widget/search/field.rs | 88 -------------------------------- src/widget/search/mod.rs | 98 ------------------------------------ src/widget/search/model.rs | 36 ------------- src/widget/search/search.svg | 11 ---- 5 files changed, 235 deletions(-) delete mode 100644 src/widget/search/field.rs delete mode 100644 src/widget/search/mod.rs delete mode 100644 src/widget/search/model.rs delete mode 100644 src/widget/search/search.svg diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 3684b954..b1b8bb43 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -163,8 +163,6 @@ pub mod row { mod scrollable; pub use scrollable::*; -pub mod search; - pub mod segmented_button; pub mod segmented_selection; diff --git a/src/widget/search/field.rs b/src/widget/search/field.rs deleted file mode 100644 index d1176559..00000000 --- a/src/widget/search/field.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::iced::{Background, Length}; -use crate::widget::{container, icon, row, text_input}; -use apply::Apply; - -/// A search field for COSMIC applications. -pub fn field( - id: iced_core::id::Id, - phrase: &str, - on_change: fn(String) -> Message, - on_clear: Message, - on_submit: Option, -) -> Field { - Field { - id, - phrase, - on_change, - on_clear, - on_submit, - } -} - -/// A search field for COSMIC applications. -#[must_use] -pub struct Field<'a, Message: 'static + Clone> { - id: iced_core::id::Id, - phrase: &'a str, - on_change: fn(String) -> Message, - on_clear: Message, - on_submit: Option, -} - -impl<'a, Message: 'static + Clone> Field<'a, Message> { - pub fn into_element(mut self) -> crate::Element<'a, Message> { - let input = text_input("", self.phrase) - .on_input(self.on_change) - .width(Length::Fill) - .id(self.id) - .on_submit_maybe(self.on_submit.take()); - - row::with_capacity(3) - .push( - icon::from_svg_bytes(&include_bytes!("search.svg")[..]) - .symbolic(true) - .icon() - .size(16), - ) - .push(input) - .push(clear_button().on_press(self.on_clear)) - .width(Length::Fixed(300.0)) - .height(Length::Fixed(38.0)) - .padding([0, 16]) - .spacing(8) - .align_items(iced::Alignment::Center) - .apply(container) - .style(crate::theme::Container::custom(active_style)) - .into() - } -} - -impl<'a, Message: 'static + Clone> From> for crate::Element<'a, Message> { - fn from(field: Field<'a, Message>) -> Self { - field.into_element() - } -} - -fn clear_button() -> crate::widget::IconButton<'static, Message> { - icon::from_name("edit-clear-symbolic") - .size(16) - .apply(crate::widget::button::icon) -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -fn active_style(theme: &crate::Theme) -> container::Appearance { - let cosmic = &theme.cosmic(); - let mut neutral_7 = cosmic.palette.neutral_7; - neutral_7.alpha = 0.25; - iced::widget::container::Appearance { - icon_color: Some(cosmic.palette.neutral_9.into()), - text_color: Some(cosmic.palette.neutral_9.into()), - background: Some(Background::Color(neutral_7.into())), - border_radius: cosmic.corner_radii.radius_m.into(), - border_width: 2.0, - border_color: cosmic.accent.focus.into(), - } -} diff --git a/src/widget/search/mod.rs b/src/widget/search/mod.rs deleted file mode 100644 index b7f1c794..00000000 --- a/src/widget/search/mod.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A COSMIC search widget -//! -//! ## Example -//! -//! Store the model in the application: -//! -//! ```ignore -//! App { -//! search: search::Model::default() -//! } -//! ``` -//! -//! Generate the element in the view: -//! -//! ```ignore -//! let search_field = search::search(&self.search, Message::Search); -//! ``` -//! -//! Handle messages in the update method: -//! -//! ```ignore -//! match message { -//! Message::Search(search::Message::Activate) => { -//! // Returns command to focus the text input. -//! return self.search.focus(); -//! } -//! Message::Search(search::Message::Changed) => { -//! self.search.phrase = phrase; -//! self.search_changed(); -//! } -//! Message::Search(search::Message::Clear) => { -//! self.search_clear(); -//! }, -//! Message::Search(search::Message::Submit) => { -//! self.search_submit(); -//! } -//! } - -mod field; -mod model; - -mod button { - use crate::widget::{container, icon}; - use apply::Apply; - - /// A search button which converts to a search [`field`] on click. - #[must_use] - pub fn button(on_press: Message) -> crate::Element<'static, Message> { - icon::from_svg_bytes(&include_bytes!("search.svg")[..]) - .symbolic(true) - .apply(crate::widget::button::icon) - .on_press(on_press) - .apply(container) - .padding([0, 0, 0, 11]) - .into() - } -} - -pub use button::button; -pub use field::{field, Field}; -pub use model::Model; - -/// Creates the COSMIC search field widget -/// -/// A button is displayed when inactive, and the search field when active. -pub fn search(model: &Model, on_emit: fn(Message) -> M) -> crate::Element { - let element = match model.state { - State::Active => field( - model.input_id.clone(), - &model.phrase, - Message::Changed, - Message::Clear, - Some(Message::Submit), - ) - .into(), - - State::Inactive => button(Message::Activate), - }; - - element.map(on_emit) -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum Message { - Activate, - Changed(String), - Clear, - Submit, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum State { - Active, - Inactive, -} diff --git a/src/widget/search/model.rs b/src/widget/search/model.rs deleted file mode 100644 index 632c05b6..00000000 --- a/src/widget/search/model.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::State; -use crate::iced; - -/// A model for managing the state of a search widget. -pub struct Model { - pub input_id: iced_core::id::Id, - pub phrase: String, - pub state: State, -} - -impl Model { - /// Focuses the search field. - pub fn focus(&mut self) -> crate::iced::Command { - self.state = State::Active; - iced::widget::text_input::focus(self.input_id.clone()) - } - - /// Check if the search field is currently active. - #[must_use] - pub fn is_active(&self) -> bool { - self.state == State::Active - } -} - -impl Default for Model { - fn default() -> Self { - Self { - input_id: iced_core::id::Id::unique(), - phrase: String::with_capacity(32), - state: State::Inactive, - } - } -} diff --git a/src/widget/search/search.svg b/src/widget/search/search.svg deleted file mode 100644 index 33b8e88e..00000000 --- a/src/widget/search/search.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - From 0bef593ba467e3b239c9db01860d43630cc937ee Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 22 Jan 2024 08:08:45 +0100 Subject: [PATCH 0081/1050] feat!(dialog): refactor and support rfd as file_chooser provider --- Cargo.toml | 56 +++--- examples/open-dialog/Cargo.toml | 7 +- examples/open-dialog/src/main.rs | 112 +++++------ justfile | 4 +- src/dialog/file_chooser/mod.rs | 322 ++++++++++++------------------- src/dialog/file_chooser/open.rs | 299 ++++++++++++++++++++++++---- src/dialog/file_chooser/save.rs | 175 +++++++++++++---- src/dialog/mod.rs | 3 +- src/lib.rs | 2 +- 9 files changed, 618 insertions(+), 362 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7c7b0b01..bd510142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,27 @@ name = "cosmic" a11y = ["iced/a11y", "iced_accessibility"] # Builds support for animated images animated-image = ["image", "dep:async-fs", "tokio?/io-util", "tokio?/fs"] +# XXX Use "a11y"; which is causing a panic currently +applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] +applet-token = [] +# Use the cosmic-settings-daemon for config handling +dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"] # Debug features debug = ["iced/debug"] # Enables pipewire support in ashpd, if ashpd is enabled pipewire = ["ashpd?/pipewire"] # Enables process spawning helper -process = ["nix"] +process = ["dep:nix"] +# Use rfd for file dialogs +rfd = ["dep:rfd"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] +# Prevents multiple separate process instances. +single-instance = ["dep:zbus", "serde", "ron"] # smol async runtime smol = ["iced/smol", "zbus?/async-io"] # Tokio async runtime -tokio = ["dep:tokio", "ashpd?/tokio", "iced/tokio", "zbus?/tokio"] +tokio = ["dep:tokio", "ashpd?/tokio", "iced/tokio", "rfd?/tokio", "zbus?/tokio"] # Wayland window support wayland = [ "ashpd?/wayland", @@ -42,35 +51,30 @@ winit_tokio = ["winit", "tokio"] winit_wgpu = ["winit", "wgpu"] # Enables XDG portal integrations xdg-portal = ["ashpd"] -# XXX Use "a11y"; which is causing a panic currently -applet = ["wayland", "tokio", "cosmic-panel-config", "ron"] -applet-token = [] -single-instance = ["dep:zbus", "serde", "ron"] -dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"] [dependencies] apply = "0.3.0" -derive_setters = "0.1.5" -lazy_static = "1.4.0" -palette = "0.7.3" -tokio = { version = "1.24.2", optional = true } -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e", optional = true } -slotmap = "1.0.6" -fraction = "0.14.0" -cosmic-config = { path = "cosmic-config" } -tracing = "0.1" -image = { version = "0.24.6", optional = true } -thiserror = "1.0.44" +ashpd = { version = "0.6.8", default-features = false, optional = true } async-fs = { version = "2.1", optional = true } -ashpd = { version = "0.6.0", default-features = false, optional = true } -url = "2.4.0" -unicode-segmentation = "1.6" -css-color = "0.2.5" -nix = { version = "0.27", features = ["process"], optional = true } -zbus = {version = "3.14.1", default-features = false, optional = true} -serde = { version = "1.0.180", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e", optional = true } +cosmic-config = { path = "cosmic-config" } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } - +css-color = "0.2.5" +derive_setters = "0.1.5" +fraction = "0.14.0" +image = { version = "0.24.6", optional = true } +lazy_static = "1.4.0" +nix = { version = "0.27", features = ["process"], optional = true } +palette = "0.7.3" +rfd = { version = "0.13.0", optional = true } +serde = { version = "1.0.180", optional = true } +slotmap = "1.0.6" +thiserror = "1.0.44" +tokio = { version = "1.24.2", optional = true } +tracing = "0.1" +unicode-segmentation = "1.6" +url = "2.4.0" +zbus = {version = "3.14.1", default-features = false, optional = true} [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.4" diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index 7e1a52dc..1cd40359 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -3,6 +3,11 @@ name = "open-dialog" version = "0.1.0" edition = "2021" +[features] +default = ["xdg-portal"] +rfd = ["libcosmic/rfd"] +xdg-portal = ["libcosmic/xdg-portal"] + [dependencies] apply = "0.3.0" tokio = { version = "1.31", features = ["full"] } @@ -13,4 +18,4 @@ url = "2.4.0" [dependencies.libcosmic] path = "../../" default-features = false -features = ["debug", "wayland", "tokio", "xdg-portal"] +features = ["debug", "wayland", "tokio"] diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index f14379fc..f8b95360 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -9,6 +9,7 @@ use cosmic::dialog::file_chooser::{self, FileFilter}; use cosmic::iced_core::Length; use cosmic::widget::button; use cosmic::{executor, iced, ApplicationExt, Element}; +use std::sync::Arc; use tokio::io::AsyncReadExt; use url::Url; @@ -26,12 +27,11 @@ fn main() -> Result<(), Box> { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { + Cancelled, CloseError, - DialogClosed, - DialogInit(file_chooser::Sender), - DialogOpened, Error(String), FileRead(Url, String), + OpenError(Arc), OpenFile, Selected(Url), } @@ -39,7 +39,6 @@ pub enum Message { /// The [`App`] stores application-specific state. pub struct App { core: Core, - open_sender: Option, file_contents: String, selected_file: Option, error_status: Option, @@ -70,7 +69,6 @@ impl cosmic::Application for App { fn init(core: Core, _input: Self::Flags) -> (Self, Command) { let mut app = App { core, - open_sender: None, file_contents: String::new(), selected_file: None, error_status: None, @@ -90,41 +88,10 @@ impl cosmic::Application for App { vec![button::suggested("Open").on_press(Message::OpenFile).into()] } - fn subscription(&self) -> cosmic::iced_futures::Subscription { - // Creates a subscription for handling open dialogs. - file_chooser::subscription(|response| match response { - file_chooser::Message::Closed => Message::DialogClosed, - file_chooser::Message::Opened => Message::DialogOpened, - file_chooser::Message::Selected(files) => match files.uris().first() { - Some(file) => Message::Selected(file.to_owned()), - None => Message::DialogClosed, - }, - file_chooser::Message::Init(sender) => Message::DialogInit(sender), - file_chooser::Message::Err(why) => { - let mut source: &dyn std::error::Error = &why; - let mut string = format!("open dialog subscription errored\n cause: {source}"); - - while let Some(new_source) = source.source() { - string.push_str(&format!("\n cause: {new_source}")); - source = new_source; - } - - Message::Error(string) - } - }) - } - fn update(&mut self, message: Self::Message) -> Command { match message { - Message::DialogClosed => { - eprintln!("dialog closed"); - } - - Message::DialogOpened => { - if let Some(sender) = self.open_sender.as_mut() { - eprintln!("requesting selection"); - return sender.response().map(|_| cosmic::app::Message::None); - } + Message::Cancelled => { + eprintln!("open file dialog cancelled"); } Message::FileRead(url, contents) => { @@ -178,29 +145,30 @@ impl cosmic::Application for App { // Creates a new open dialog. Message::OpenFile => { - if let Some(sender) = self.open_sender.as_mut() { - if let Some(dialog) = file_chooser::open_file() { - eprintln!("opening new dialog"); + return cosmic::command::future(async move { + eprintln!("opening new dialog"); - return dialog - // Sets title of the dialog window. - .title("Choose a file".into()) - // Sets the label of the accept button. - .accept_label("_Open".into()) - // Exclude directories from file selection. - .include_directories(false) - // Defines whether to block the main window while requesting input. - .modal(false) - // Only accept one file as input. - .multiple_files(false) - // Accept only plain text files - .filter(FileFilter::new("Text files").mimetype("text/plain")) - // Emits the dialog to our sender - .create(sender) - // Ignores the output because it's empty. - .map(|_| cosmic::app::message::none()); + #[cfg(feature = "rfd")] + let filter = FileFilter::new("Text files").extension("txt"); + + #[cfg(feature = "xdg-portal")] + let filter = FileFilter::new("Text files").glob("*.txt"); + + let dialog = file_chooser::open::Dialog::new() + // Sets title of the dialog window. + .title("Choose a file") + // Accept only plain text files + .filter(filter); + + match dialog.open_file().await { + Ok(response) => Message::Selected(response.url().to_owned()), + + Err(file_chooser::Error::Cancelled) => Message::Cancelled, + + Err(why) => Message::OpenError(Arc::new(why)), } - } + }) + .map(cosmic::app::Message::App); } // Displays an error in the application's warning bar. @@ -208,15 +176,24 @@ impl cosmic::Application for App { self.error_status = Some(why); } - // Closes the warning bar, if it was shown. - Message::CloseError => { - self.error_status = None; + // Displays an error in the application's warning bar. + Message::OpenError(why) => { + if let Some(why) = Arc::into_inner(why) { + let mut source: &dyn std::error::Error = &why; + let mut string = + format!("open dialog subscription errored\n cause: {source}"); + + while let Some(new_source) = source.source() { + string.push_str(&format!("\n cause: {new_source}")); + source = new_source; + } + + self.error_status = Some(string); + } } - // The open dialog. subscription provides this on register. - Message::DialogInit(sender) => { - eprintln!("dialog subscription enabled"); - self.open_sender = Some(sender); + Message::CloseError => { + self.error_status = None; } } @@ -232,7 +209,8 @@ impl cosmic::Application for App { .on_close(Message::CloseError) .into(), ); - content.push(iced::widget::vertical_space(Length::Fixed(12.0)).into()) + + content.push(iced::widget::vertical_space(Length::Fixed(12.0)).into()); } content.push(if self.selected_file.is_none() { diff --git a/justfile b/justfile index 18e06b51..dcddf7e7 100644 --- a/justfile +++ b/justfile @@ -11,10 +11,10 @@ check-examples *args: done check-wayland *args: - cargo clippy --no-deps --features="wayland,tokio" {{args}} -- {{clippy_args}} + cargo clippy --no-deps --features="wayland,tokio,xdg-portal" {{args}} -- {{clippy_args}} check-winit *args: - cargo clippy --no-deps --features="winit,tokio" {{args}} -- {{clippy_args}} + cargo clippy --no-deps --features="winit,tokio,xdg-portal" {{args}} -- {{clippy_args}} # Runs a check with JSON message format for IDE integration check-json: (check '--message-format=json') diff --git a/src/dialog/file_chooser/mod.rs b/src/dialog/file_chooser/mod.rs index ccb15c42..0f328e32 100644 --- a/src/dialog/file_chooser/mod.rs +++ b/src/dialog/file_chooser/mod.rs @@ -2,219 +2,147 @@ // SPDX-License-Identifier: MPL-2.0 //! Dialogs for opening and save files. +//! +//! # Features +//! +//! - On Linux, the `xdg-portal` feature will use XDG Portal dialogs. +//! - Alternatively, `rfd` can be used for platform support beyond Linux. +//! +//! # Open a file +//! +//! ```no_run +//! cosmic::command::future(async { +//! use cosmic::dialog::file_chooser; +//! +//! let dialog = file_chooser::open::Dialog::new() +//! .title("Choose a file"); +//! +//! match dialog.open_file().await { +//! Ok(response) => println!("selected to open {:?}", response.url()), +//! +//! Err(file_chooser::Error::Cancelled) => (), +//! +//! Err(why) => eprintln!("error selecting file to open: {why:?}") +//! } +//! }); +//! ``` +//! +//! # Open multiple files +//! +//! ```no_run +//! cosmic::command::future(async { +//! use cosmic::dialog::file_chooser; +//! +//! let dialog = file_chooser::open::Dialog::new() +//! .title("Choose multiple files"); +//! +//! match dialog.open_files().await { +//! Ok(response) => println!("selected to open {:?}", response.urls()), +//! +//! Err(file_chooser::Error::Cancelled) => (), +//! +//! Err(why) => eprintln!("error selecting file(s) to open: {why:?}") +//! } +//! }); +//! ``` +//! +//! # Open a folder +//! +//! ```no_run +//! cosmic::command::future(async { +//! use cosmic::dialog::file_chooser; +//! +//! let dialog = file_chooser::open::Dialog::new() +//! .title("Choose a folder"); +//! +//! match dialog.open_folder().await { +//! Ok(response) => println!("selected to open {:?}", response.url()), +//! +//! Err(file_chooser::Error::Cancelled) => (), +//! +//! Err(why) => eprintln!("error selecting folder to open: {why:?}") +//! } +//! }); +//! ``` +//! +//! # Open multiple folders +//! +//! ```no_run +//! cosmic::command::future(async { +//! use cosmic::dialog::file_chooser; +//! +//! let dialog = file_chooser::open::Dialog::new() +//! .title("Choose a folder"); +//! +//! match dialog.open_folders().await { +//! Ok(response) => println!("selected to open {:?}", response.urls()), +//! +//! Err(file_chooser::Error::Cancelled) => (), +//! +//! Err(why) => eprintln!("error selecting folder(s) to open: {why:?}") +//! } +//! }); +//! ``` +/// Open file dialog. pub mod open; + +/// Save file dialog. pub mod save; -pub use ashpd::desktop::file_chooser::{Choice, FileFilter, SelectedFiles}; -use iced::futures::{channel, SinkExt, StreamExt}; -use iced::{Command, Subscription}; -use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "xdg-portal")] +pub use ashpd::desktop::file_chooser::{Choice, FileFilter}; + use thiserror::Error; -/// Prevents duplicate file chooser dialog requests. -static OPENED: AtomicBool = AtomicBool::new(false); - -/// Whether a file chooser dialog is currently active. -fn dialog_active() -> bool { - OPENED.load(Ordering::Relaxed) +/// A file filter, to limit the available file choices to certain extensions. +#[cfg(feature = "rfd")] +#[must_use] +pub struct FileFilter { + description: String, + extensions: Vec, } -/// Sets the existence of a file chooser dialog. -fn dialog_active_set(value: bool) { - OPENED.store(value, Ordering::SeqCst); -} - -/// Creates an [`open::Dialog`] if no other file chooser exists. -pub fn open_file() -> Option { - if dialog_active() { - None - } else { - Some(open::Dialog::new()) - } -} - -/// Creates a [`save::Dialog`] if no other file chooser exists. -pub fn save_file() -> Option { - if dialog_active() { - None - } else { - Some(save::Dialog::new()) - } -} - -/// Creates a subscription for file chooser events. -pub fn subscription(handle: H) -> Subscription -where - M: Send + 'static, - H: Fn(Message) -> M + Send + Sync + 'static, -{ - let type_id = std::any::TypeId::of::>(); - - iced::subscription::channel(type_id, 1, move |output| async move { - let mut state = Handler { - active: None, - handle, - output, - }; - - loop { - let (sender, mut receiver) = channel::mpsc::channel(1); - - state.emit(Message::Init(Sender(sender))).await; - - while let Some(request) = receiver.next().await { - match request { - Request::Close => state.close().await, - - Request::Open(dialog) => { - state.open(dialog).await; - dialog_active_set(false); - } - - Request::Save(dialog) => { - state.save(dialog).await; - dialog_active_set(false); - } - - Request::Response => state.response().await, - } - } +#[cfg(feature = "rfd")] +impl FileFilter { + pub fn new(description: impl Into) -> Self { + Self { + description: description.into(), + extensions: Vec::new(), } - }) + } + + pub fn extension(mut self, extension: impl Into) -> Self { + self.extensions.push(extension.into()); + self + } } /// Errors that my occur when interacting with the file chooser subscription #[derive(Debug, Error)] pub enum Error { + #[error("dialog request cancelled")] + Cancelled, #[error("dialog close failed")] - Close(#[source] ashpd::Error), - #[error("dialog open failed")] - Open(#[source] ashpd::Error), + Close(#[source] DialogError), + #[error("open dialog failed")] + Open(#[source] DialogError), #[error("dialog response failed")] - Response(#[source] ashpd::Error), + Response(#[source] DialogError), + #[error("save dialog failed")] + Save(#[source] DialogError), + #[error("could not set directory")] + SetDirectory(#[source] DialogError), + #[error("could not set absolute path for file name")] + SetAbsolutePath(#[source] DialogError), + #[error("path from dialog was not absolute")] + UrlAbsolute, } -/// Requests for the file chooser subscription -enum Request { - Close, - Open(open::Dialog), - Save(save::Dialog), - Response, -} +#[cfg(feature = "xdg-portal")] +pub type DialogError = ashpd::Error; -/// Messages from the file chooser subscription. -pub enum Message { - Closed, - Err(Error), - Init(Sender), - Opened, - Selected(SelectedFiles), -} - -/// Sends requests to the file chooser subscription. -#[derive(Clone, Debug)] -pub struct Sender(channel::mpsc::Sender); - -impl Sender { - /// Creates a [`Command`] that closes a file chooser dialog. - pub fn close(&mut self) -> Command<()> { - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Close).await; - () - }) - } - - /// Creates a [`Command`] that opens the file chooser. - pub fn open(&mut self, dialog: open::Dialog) -> Command<()> { - dialog_active_set(true); - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Open(dialog)).await; - () - }) - } - - /// Creates a [`Command`] that requests the response from a file chooser dialog. - pub fn response(&mut self) -> Command<()> { - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Response).await; - () - }) - } - - /// Creates a [`Command`] that opens a new save file dialog. - pub fn save(&mut self, dialog: save::Dialog) -> Command<()> { - dialog_active_set(true); - let mut sender = self.0.clone(); - - crate::command::future(async move { - let _res = sender.send(Request::Save(dialog)).await; - () - }) - } -} - -struct Handler M> { - active: Option>, - handle: Handle, - output: channel::mpsc::Sender, -} - -impl M> Handler { - /// Emits close request if there is an active dialog request. - async fn close(&mut self) { - if let Some(request) = self.active.take() { - if let Err(why) = request.close().await { - self.emit(Message::Err(Error::Close(why))).await; - } - } - } - - async fn emit(&mut self, response: Message) { - let _res = self.output.send((self.handle)(response)).await; - } - - /// Creates a new dialog, and closes any prior active dialogs. - async fn open(&mut self, dialog: open::Dialog) { - let response = match open::create(dialog).await { - Ok(request) => { - self.active = Some(request); - Message::Opened - } - Err(why) => Message::Err(Error::Open(why)), - }; - - self.emit(response).await; - } - - /// Collects selected files from the active dialog. - async fn response(&mut self) { - if let Some(request) = self.active.as_ref() { - let response = match request.response() { - Ok(selected) => Message::Selected(selected), - Err(why) => Message::Err(Error::Response(why)), - }; - - self.emit(response).await; - } - } - - /// Creates a new dialog, and closes any prior active dialogs. - async fn save(&mut self, dialog: save::Dialog) { - let response = match save::create(dialog).await { - Ok(request) => { - self.active = Some(request); - Message::Opened - } - Err(why) => Message::Err(Error::Open(why)), - }; - - self.emit(response).await; - } -} +#[cfg(feature = "rfd")] +#[derive(Debug, Error)] +#[error("no file selected")] +pub struct DialogError {} diff --git a/src/dialog/file_chooser/open.rs b/src/dialog/file_chooser/open.rs index 20d4176b..80e5ffbe 100644 --- a/src/dialog/file_chooser/open.rs +++ b/src/dialog/file_chooser/open.rs @@ -6,85 +6,318 @@ //! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) //! example in our repository. -use derive_setters::Setters; -use iced::Command; +#[cfg(feature = "xdg-portal")] +pub use portal::{file, files, folder, folders, FileResponse, MultiFileResponse}; -/// A builder for an open file dialog, passed as a request by a [`Sender`] -#[derive(Setters)] +#[cfg(feature = "rfd")] +pub use rust_fd::{file, files, folder, folders, FileResponse, MultiFileResponse}; + +use super::Error; +use std::path::PathBuf; + +/// A builder for an open file dialog +#[derive(derive_setters::Setters)] #[must_use] pub struct Dialog { /// The label for the dialog's window title. + #[setters(into)] title: String, /// The label for the accept button. Mnemonic underlines are allowed. - #[setters(strip_option)] + #[cfg(feature = "xdg-portal")] + #[setters(skip)] accept_label: Option, - /// Whether to select for folders instead of files. Default is to select files. - include_directories: bool, + /// Sets the starting directory of the dialog. + #[setters(into, strip_option)] + #[allow(dead_code)] // TODO: ashpd does not expose this yet + directory: Option, + + /// Set starting file name of the dialog. + #[setters(into, strip_option)] + #[allow(dead_code)] // TODO: ashpd does not expose this yet + file_name: Option, /// Modal dialogs require user input before continuing the program. + #[cfg(feature = "xdg-portal")] + #[setters(skip)] modal: bool, - /// Whether to allow selection of multiple files. Default is no. - multiple_files: bool, - /// Adds a list of choices. + #[cfg(feature = "xdg-portal")] + #[setters(skip)] choices: Vec, /// Specifies the default file filter. - #[setters(into)] + #[cfg(feature = "xdg-portal")] + #[setters(skip)] current_filter: Option, /// A collection of file filters. - filters: Vec, + #[setters(skip)] + pub(self) filters: Vec, } impl Dialog { - pub(super) const fn new() -> Self { + pub const fn new() -> Self { Self { title: String::new(), + #[cfg(feature = "xdg-portal")] accept_label: None, - include_directories: false, + directory: None, + file_name: None, + #[cfg(feature = "xdg-portal")] modal: true, - multiple_files: false, + #[cfg(feature = "xdg-portal")] current_filter: None, + #[cfg(feature = "xdg-portal")] choices: Vec::new(), filters: Vec::new(), } } - /// Creates a [`Command`] which opens the dialog. - pub fn create(self, sender: &mut super::Sender) -> Command<()> { - sender.open(self) + /// The label for the accept button. Mnemonic underlines are allowed. + #[cfg(feature = "xdg-portal")] + pub fn accept_label(mut self, label: impl Into) -> Self { + self.accept_label = Some(label.into()); + self } /// Adds a choice. + #[cfg(feature = "xdg-portal")] pub fn choice(mut self, choice: impl Into) -> Self { self.choices.push(choice.into()); self } + /// Specifies the default file filter. + #[cfg(feature = "xdg-portal")] + pub fn current_filter(mut self, filter: impl Into) -> Self { + self.current_filter = Some(filter.into()); + self + } + /// Adds a files filter. pub fn filter(mut self, filter: impl Into) -> Self { self.filters.push(filter.into()); self } + + /// Modal dialogs require user input before continuing the program. + #[cfg(feature = "xdg-portal")] + pub fn modal(mut self, modal: bool) -> Self { + self.modal = modal; + self + } + + /// Create an open file dialog. + pub async fn open_file(self) -> Result { + file(self).await + } + + /// Create an open file dialog with multiple file select. + pub async fn open_files(self) -> Result { + files(self).await + } + + /// Create an open folder dialog. + pub async fn open_folder(self) -> Result { + folder(self).await + } + + /// Create an open folder dialog with multi file select. + pub async fn open_folders(self) -> Result { + folders(self).await + } } -/// Creates a new file dialog, and begins to await its responses. -pub(super) async fn create( - dialog: Dialog, -) -> ashpd::Result> { - ashpd::desktop::file_chooser::OpenFileRequest::default() - .title(Some(dialog.title.as_str())) - .accept_label(dialog.accept_label.as_deref()) - .directory(dialog.include_directories) - .modal(dialog.modal) - .multiple(dialog.multiple_files) - .choices(dialog.choices) - .filters(dialog.filters) - .current_filter(dialog.current_filter) - .send() - .await +#[cfg(feature = "xdg-portal")] +mod portal { + use super::Dialog; + use crate::dialog::file_chooser::Error; + use ashpd::desktop::file_chooser::SelectedFiles; + use url::Url; + + fn error_or_cancel(error: ashpd::Error) -> Error { + if let ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled) = error { + Error::Cancelled + } else { + Error::Open(error) + } + } + + /// Creates a new file dialog, and begins to await its responses. + #[cfg(feature = "xdg-portal")] + pub async fn create( + dialog: super::Dialog, + folders: bool, + multiple: bool, + ) -> Result, Error> { + // TODO: Set window identifier + ashpd::desktop::file_chooser::OpenFileRequest::default() + .title(Some(dialog.title.as_str())) + .accept_label(dialog.accept_label.as_deref()) + .directory(folders) + .modal(dialog.modal) + .multiple(multiple) + .choices(dialog.choices) + .filters(dialog.filters) + .current_filter(dialog.current_filter) + .send() + .await + .map_err(error_or_cancel) + } + + fn file_response( + request: ashpd::desktop::Request, + ) -> Result { + request + .response() + .map(FileResponse) + .map_err(error_or_cancel) + } + + fn multi_file_response( + request: ashpd::desktop::Request, + ) -> Result { + request + .response() + .map(MultiFileResponse) + .map_err(error_or_cancel) + } + + pub async fn file(dialog: Dialog) -> Result { + file_response(create(dialog, false, false).await?) + } + + pub async fn files(dialog: Dialog) -> Result { + multi_file_response(create(dialog, false, true).await?) + } + + pub async fn folder(dialog: Dialog) -> Result { + file_response(create(dialog, true, false).await?) + } + + pub async fn folders(dialog: Dialog) -> Result { + multi_file_response(create(dialog, true, true).await?) + } + + /// A dialog response containing the selected file or folder. + pub struct FileResponse(pub SelectedFiles); + + impl FileResponse { + pub fn choices(&self) -> &[(String, String)] { + self.0.choices() + } + + pub fn url(&self) -> &Url { + self.0.uris().first().expect("no files selected") + } + } + + /// A dialog response containing the selected file(s) or folder(s). + pub struct MultiFileResponse(pub SelectedFiles); + + impl MultiFileResponse { + pub fn choices(&self) -> &[(String, String)] { + self.0.choices() + } + + pub fn urls(&self) -> &[Url] { + self.0.uris() + } + } +} + +#[cfg(feature = "rfd")] +mod rust_fd { + use super::Dialog; + use crate::dialog::file_chooser::Error; + use url::Url; + + pub fn create(dialog: Dialog) -> rfd::AsyncFileDialog { + let mut builder = rfd::AsyncFileDialog::new().set_title(dialog.title); + + if let Some(directory) = dialog.directory { + builder = builder.set_directory(directory); + } + + if let Some(file_name) = dialog.file_name { + builder = builder.set_file_name(file_name); + } + + for filter in dialog.filters { + builder = builder.add_filter(filter.description, &filter.extensions); + } + + builder + } + + fn file_response(request: Option) -> Result { + if let Some(handle) = request { + let url = Url::from_file_path(handle.path()).map_err(|_| Error::UrlAbsolute)?; + + return Ok(FileResponse(url)); + } + + Err(Error::Cancelled) + } + + fn multi_file_response( + request: Option>, + ) -> Result { + if let Some(handles) = request { + let mut urls = Vec::with_capacity(handles.len()); + + for handle in &handles { + urls.push(Url::from_file_path(handle.path()).map_err(|()| Error::UrlAbsolute)?); + } + + return Ok(MultiFileResponse(urls)); + } + + Err(Error::Cancelled) + } + + pub async fn file(dialog: Dialog) -> Result { + file_response(create(dialog).pick_file().await) + } + + pub async fn files(dialog: Dialog) -> Result { + multi_file_response(create(dialog).pick_files().await) + } + + pub async fn folder(dialog: Dialog) -> Result { + file_response(create(dialog).pick_folder().await) + } + + pub async fn folders(dialog: Dialog) -> Result { + multi_file_response(create(dialog).pick_folders().await) + } + + /// A dialog response containing the selected file or folder. + pub struct FileResponse(Url); + + impl FileResponse { + pub fn choices(&self) -> &[(String, String)] { + &[] + } + + pub fn url(&self) -> &Url { + &self.0 + } + } + + /// A dialog response containing the selected file(s) or folder(s). + pub struct MultiFileResponse(Vec); + + impl MultiFileResponse { + pub fn choices(&self) -> &[(String, String)] { + &[] + } + + pub fn urls(&self) -> &[Url] { + &self.0 + } + } } diff --git a/src/dialog/file_chooser/save.rs b/src/dialog/file_chooser/save.rs index 0729b0f7..63c07340 100644 --- a/src/dialog/file_chooser/save.rs +++ b/src/dialog/file_chooser/save.rs @@ -6,94 +6,201 @@ //! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) //! example in our repository. -use derive_setters::Setters; -use iced::Command; -use std::path::{Path, PathBuf}; +#[cfg(feature = "xdg-portal")] +pub use portal::{file, Response}; -/// A builder for an save file dialog, passed as a request by a [`Sender`] -#[derive(Setters)] +#[cfg(feature = "rfd")] +pub use rust_fd::{file, Response}; + +use super::Error; +use std::path::PathBuf; + +/// A builder for an save file dialog. +#[derive(derive_setters::Setters)] #[must_use] pub struct Dialog { /// The label for the dialog's window title. title: String, /// The label for the accept button. Mnemonic underlines are allowed. - #[setters(strip_option)] + #[cfg(feature = "xdg-portal")] + #[setters(skip)] accept_label: Option, /// Modal dialogs require user input before continuing the program. + #[cfg(feature = "xdg-portal")] + #[setters(skip)] modal: bool, - /// Sets the current file name. + /// Set starting file name of the dialog. #[setters(strip_option)] - current_name: Option, + file_name: Option, - /// Sets the current folder. + /// Sets the starting directory of the dialog. #[setters(strip_option)] - current_folder: Option, + directory: Option, /// Sets the absolute path of the file - #[setters(strip_option)] + #[cfg(feature = "xdg-portal")] + #[setters(skip)] current_file: Option, /// Adds a list of choices. + #[cfg(feature = "xdg-portal")] + #[setters(skip)] choices: Vec, /// Specifies the default file filter. - #[setters(into)] + #[cfg(feature = "xdg-portal")] + #[setters(skip)] current_filter: Option, /// A collection of file filters. + #[setters(skip)] filters: Vec, } impl Dialog { - pub(super) const fn new() -> Self { + pub const fn new() -> Self { Self { title: String::new(), + #[cfg(feature = "xdg-portal")] accept_label: None, + #[cfg(feature = "xdg-portal")] modal: true, - current_name: None, - current_folder: None, + file_name: None, + directory: None, + #[cfg(feature = "xdg-portal")] current_file: None, + #[cfg(feature = "xdg-portal")] current_filter: None, + #[cfg(feature = "xdg-portal")] choices: Vec::new(), filters: Vec::new(), } } - /// Creates a [`Command`] which opens the dialog. - pub fn create(self, sender: &mut super::Sender) -> Command<()> { - sender.save(self) + /// The label for the accept button. Mnemonic underlines are allowed. + #[cfg(feature = "xdg-portal")] + pub fn accept_label(mut self, label: impl Into) -> Self { + self.accept_label = Some(label.into()); + self } /// Adds a choice. + #[cfg(feature = "xdg-portal")] pub fn choice(mut self, choice: impl Into) -> Self { self.choices.push(choice.into()); self } + /// Set the current file filter. + #[cfg(feature = "xdg-portal")] + pub fn current_filter(mut self, filter: impl Into) -> Self { + self.current_filter = Some(filter.into()); + self + } + /// Adds a files filter. pub fn filter(mut self, filter: impl Into) -> Self { self.filters.push(filter.into()); self } + + /// Modal dialogs require user input before continuing the program. + #[cfg(feature = "xdg-portal")] + pub fn modal(mut self, modal: bool) -> Self { + self.modal = modal; + self + } + + /// Create a save file dialog request. + pub async fn save_file(self) -> Result { + file(self).await + } } -/// Creates a new file dialog, and begins to await its responses. -pub(super) async fn create( - dialog: Dialog, -) -> ashpd::Result> { - ashpd::desktop::file_chooser::SaveFileRequest::default() - .title(Some(dialog.title.as_str())) - .accept_label(dialog.accept_label.as_deref()) - .modal(dialog.modal) - .choices(dialog.choices) - .filters(dialog.filters) - .current_filter(dialog.current_filter) - .current_name(dialog.current_name.as_deref()) - .current_folder::<&Path>(dialog.current_folder.as_deref())? - .current_file::<&Path>(dialog.current_file.as_deref())? - .send() - .await +#[cfg(feature = "xdg-portal")] +mod portal { + use super::Dialog; + use crate::dialog::file_chooser::Error; + use ashpd::desktop::file_chooser::SelectedFiles; + use std::path::Path; + use url::Url; + + /// Create a save file dialog request. + pub async fn file(dialog: Dialog) -> Result { + ashpd::desktop::file_chooser::SaveFileRequest::default() + .title(Some(dialog.title.as_str())) + .accept_label(dialog.accept_label.as_deref()) + .modal(dialog.modal) + .choices(dialog.choices) + .filters(dialog.filters) + .current_filter(dialog.current_filter) + .current_name(dialog.file_name.as_deref()) + .current_folder::<&Path>(dialog.directory.as_deref()) + .map_err(Error::SetDirectory)? + .current_file::<&Path>(dialog.current_file.as_deref()) + .map_err(Error::SetAbsolutePath)? + .send() + .await + .map_err(Error::Save)? + .response() + .map_err(Error::Save) + .map(Response) + } + + /// A dialog response containing the selected file or folder. + pub struct Response(pub SelectedFiles); + + impl Response { + pub fn choices(&self) -> &[(String, String)] { + self.0.choices() + } + + pub fn url(&self) -> Option<&Url> { + self.0.uris().first() + } + } +} + +#[cfg(feature = "rfd")] +mod rust_fd { + use super::Dialog; + use crate::dialog::file_chooser::Error; + use url::Url; + + /// Create a save file dialog request. + pub async fn file(dialog: Dialog) -> Result { + let mut request = rfd::AsyncFileDialog::new().set_title(dialog.title); + + if let Some(directory) = dialog.directory { + request = request.set_directory(directory); + } + + if let Some(file_name) = dialog.file_name { + request = request.set_file_name(file_name); + } + + for filter in dialog.filters { + request = request.add_filter(filter.description, &filter.extensions); + } + + if let Some(handle) = request.save_file().await { + let url = Url::from_file_path(handle.path()).map_err(|_| Error::UrlAbsolute)?; + + return Ok(Response(Some(url))); + } + + Ok(Response(None)) + } + + /// A dialog response containing the selected file or folder. + pub struct Response(Option); + + impl Response { + pub fn url(&self) -> Option<&Url> { + self.0.as_ref() + } + } } diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs index dc753096..66b3cec7 100644 --- a/src/dialog/mod.rs +++ b/src/dialog/mod.rs @@ -3,6 +3,7 @@ //! Create dialogs for retrieving user input. -pub use ashpd::WindowIdentifier; +#[cfg(feature = "xdg-portal")] +pub use ashpd; pub mod file_chooser; diff --git a/src/lib.rs b/src/lib.rs index b00ad829..258b4e2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,7 @@ pub mod command; pub use cosmic_config; pub use cosmic_theme; -#[cfg(feature = "xdg-portal")] +#[cfg(any(feature = "xdg-portal", feature = "rfd"))] pub mod dialog; pub mod executor; From 05f8ffeef10efbd17b3fb2cbfb32b6b284f15c98 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 23 Jan 2024 02:36:05 +0100 Subject: [PATCH 0082/1050] fix(segmented_button): clip text that overlaps with close button --- src/widget/segmented_button/widget.rs | 39 ++++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 7e8c6973..b0c72a43 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -694,10 +694,22 @@ where alignment::Horizontal::Center }; + // Whether to show the close button on this tab. + let show_close_button = + (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) + && self.model.is_closable(key); + + // Width of the icon used by the close button, which we will subtract from the text bounds. + let close_icon_width = if show_close_button { + f32::from(self.close_icon.size) + } else { + 0.0 + }; + if let Some(text) = self.model.text(key) { bounds.y = y; - // Draw the text in this button. + // Draw the text for this segmented button or tab. renderer.fill_text( iced_core::text::Text { content: text, @@ -711,25 +723,26 @@ where }, bounds.position(), status_appearance.text_color, - *viewport, + Rectangle { + width: bounds.width - close_icon_width - 16.0, + ..original_bounds + }, ); } - let show_close_button = - (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) - && self.model.is_closable(key); - - // Draw a close button if this is set. + // Draw a close button if set. if show_close_button { - let width = f32::from(self.close_icon.size); - let icon_bounds = close_bounds(original_bounds, width, self.button_padding); + let close_button_bounds = + close_bounds(original_bounds, close_icon_width, self.button_padding); + let mut layout_node = layout::Node::new(Size { - width: icon_bounds.width, - height: icon_bounds.height, + width: close_button_bounds.width, + height: close_button_bounds.height, }); + layout_node.move_to(Point { - x: icon_bounds.x, - y: icon_bounds.y, + x: close_button_bounds.x, + y: close_button_bounds.y, }); Widget::::draw( From ca92049ab66c338b48a2a2eeb85b46e2e597eb13 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 23 Jan 2024 14:31:37 +0100 Subject: [PATCH 0083/1050] feat(segmented_button): paginate tabs when width is too narrow --- src/widget/segmented_button/horizontal.rs | 45 +- src/widget/segmented_button/vertical.rs | 9 +- src/widget/segmented_button/widget.rs | 534 +++++++++++++++++----- 3 files changed, 469 insertions(+), 119 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 8fa28c7c..7b8b374a 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -44,18 +44,38 @@ where } #[allow(clippy::cast_precision_loss)] - fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { - let num = self.model.items.len(); - if num != 0 { - let spacing = f32::from(self.spacing); - bounds.width = ((num as f32).mul_add(-spacing, bounds.width) + spacing) / num as f32; + fn variant_button_bounds( + &self, + state: &LocalState, + mut bounds: Rectangle, + nth: usize, + ) -> Option { + let num = state.buttons_visible; - if nth != 0 { - bounds.x += (nth as f32).mul_add(bounds.width, nth as f32 * spacing); + // Do not display tabs that are currently hidden due to width constraints. + if state.collapsed && nth < state.buttons_offset { + return None; + } + + if num != 0 { + let offset_width; + (bounds.x, offset_width) = if state.collapsed { + (bounds.x + 16.0, 32.0) + } else { + (bounds.x, 0.0) + }; + + let spacing = f32::from(self.spacing); + bounds.width = ((num as f32).mul_add(-spacing, bounds.width - offset_width) + spacing) + / num as f32; + + if nth != state.buttons_offset { + let pos = (nth - state.buttons_offset) as f32; + bounds.x += pos.mul_add(bounds.width, pos * spacing); } } - bounds + Some(bounds) } #[allow(clippy::cast_precision_loss)] @@ -81,6 +101,15 @@ where .height(Length::Fixed(height)) .resolve(Size::new(width, height)); + let actual_width = size.width as usize; + let minimum_width = self.minimum_button_width as usize * self.model.items.len(); + + state.buttons_visible = num; + state.collapsed = actual_width < minimum_width; + if state.collapsed { + state.buttons_visible = (actual_width / self.minimum_button_width as usize).min(num); + } + layout::Node::new(size) } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 827f6cf4..c6c6b077 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -45,7 +45,12 @@ where } #[allow(clippy::cast_precision_loss)] - fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { + fn variant_button_bounds( + &self, + _state: &LocalState, + mut bounds: Rectangle, + nth: usize, + ) -> Option { let num = self.model.items.len(); if num != 0 { let spacing = f32::from(self.spacing); @@ -56,7 +61,7 @@ where } } - bounds + Some(bounds) } #[allow(clippy::cast_precision_loss)] diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index b0c72a43..11b999f7 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -19,37 +19,9 @@ use slotmap::{Key, SecondaryMap}; use std::marker::PhantomData; use std::time::{Duration, Instant}; -/// State that is maintained by each individual widget. -#[derive(Default)] -pub struct LocalState { - /// The first focusable key. - first: Entity, - /// If the widget is focused or not. - focused: bool, - /// The key inside the widget that is currently focused. - focused_key: Entity, - /// The ID of the button that is being hovered. Defaults to null. - hovered: Entity, - /// The paragraphs for each text. - paragraphs: SecondaryMap, - /// Time since last tab activation from wheel movements. - wheel_timestamp: Option, -} - -impl operation::Focusable for LocalState { - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self) { - self.focused = true; - self.focused_key = self.first; - } - - fn unfocus(&mut self) { - self.focused = false; - self.focused_key = Entity::default(); - } +/// A command that focuses a segmented item stored in a widget. +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id.0)) } /// Isolates variant-specific behaviors from [`SegmentedButton`]. @@ -61,7 +33,12 @@ pub trait SegmentedVariant { ) -> super::Appearance; /// Calculates the bounds for the given button by its position. - fn variant_button_bounds(&self, bounds: Rectangle, position: usize) -> Rectangle; + fn variant_button_bounds( + &self, + state: &LocalState, + bounds: Rectangle, + position: usize, + ) -> Option; /// Calculates the layout of this variant. fn variant_layout( @@ -95,6 +72,8 @@ where pub(super) button_height: u16, /// Spacing between icon and text in button. pub(super) button_spacing: u16, + /// Minimum width of a button. + pub(super) minimum_button_width: u16, /// Spacing for each indent. pub(super) indent_spacing: u16, /// Desired font for active tabs. @@ -132,7 +111,6 @@ where Model: Selectable, SelectionMode: Default, { - #[must_use] pub fn new(model: &'a Model) -> Self { Self { model, @@ -142,6 +120,7 @@ where button_padding: [4, 4, 4, 4], button_height: 32, button_spacing: 4, + minimum_button_width: 150, indent_spacing: 16, font_active: None, font_hovered: None, @@ -181,52 +160,139 @@ where /// Focus the previous item in the widget. fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { - let mut keys = self.model.order.iter().copied().rev(); + match state.focused_item { + Focus::Tab(entity) => { + let mut keys = self.iterate_visible_tabs(state).rev(); - while let Some(key) = keys.next() { - if key == state.focused_key { - for key in keys { - // Skip disabled buttons. - if !self.is_enabled(key) { - continue; + while let Some(key) = keys.next() { + if key == entity { + for key in keys { + // Skip disabled buttons. + if !self.is_enabled(key) { + continue; + } + + state.focused_item = Focus::Tab(key); + return event::Status::Captured; + } + + break; } - - state.focused_key = key; - return event::Status::Captured; } - break; + if self.prev_tab_sensitive(state) { + state.focused_item = Focus::PrevButton; + return event::Status::Captured; + } } + + Focus::NextButton => { + if let Some(last) = self.last_tab(state) { + state.focused_item = Focus::Tab(last); + return event::Status::Captured; + } + } + + Focus::None => { + if self.next_tab_sensitive(state) { + state.focused_item = Focus::NextButton; + return event::Status::Captured; + } else if let Some(last) = self.last_tab(state) { + state.focused_item = Focus::Tab(last); + return event::Status::Captured; + } + } + + Focus::PrevButton | Focus::Set => (), } - state.focused_key = Entity::default(); + state.focused_item = Focus::None; event::Status::Ignored } /// Focus the next item in the widget. fn focus_next(&mut self, state: &mut LocalState) -> event::Status { - let mut keys = self.model.order.iter().copied(); + match state.focused_item { + Focus::Tab(entity) => { + let mut keys = self.iterate_visible_tabs(state); + while let Some(key) = keys.next() { + if key == entity { + for key in keys { + // Skip disabled buttons. + if !self.is_enabled(key) { + continue; + } - while let Some(key) = keys.next() { - if key == state.focused_key { - for key in keys { - // Skip disabled buttons. - if !self.is_enabled(key) { - continue; + state.focused_item = Focus::Tab(key); + return event::Status::Captured; + } + + break; } - - state.focused_key = key; - return event::Status::Captured; } - break; + if self.next_tab_sensitive(state) { + state.focused_item = Focus::NextButton; + return event::Status::Captured; + } } + + Focus::PrevButton => { + if let Some(first) = self.first_tab(state) { + state.focused_item = Focus::Tab(first); + return event::Status::Captured; + } + } + + Focus::None => { + if self.prev_tab_sensitive(state) { + state.focused_item = Focus::PrevButton; + return event::Status::Captured; + } else if let Some(first) = self.first_tab(state) { + state.focused_item = Focus::Tab(first); + return event::Status::Captured; + } + } + + Focus::NextButton | Focus::Set => (), } - state.focused_key = Entity::default(); + state.focused_item = Focus::None; event::Status::Ignored } + fn iterate_visible_tabs<'b>( + &'b self, + state: &LocalState, + ) -> impl DoubleEndedIterator + 'b { + self.model + .order + .iter() + .copied() + .skip(state.buttons_offset) + .take(state.buttons_visible) + } + + fn first_tab(&self, state: &LocalState) -> Option { + self.model.order.get(state.buttons_offset).copied() + } + + fn last_tab(&self, state: &LocalState) -> Option { + self.model + .order + .get(state.buttons_offset + state.buttons_visible) + .copied() + } + + #[allow(clippy::unused_self)] + fn prev_tab_sensitive(&self, state: &LocalState) -> bool { + state.buttons_offset > 0 + } + + fn next_tab_sensitive(&self, state: &LocalState) -> bool { + state.buttons_offset < self.model.order.len() - state.buttons_visible + } + pub(super) fn max_button_dimensions( &self, state: &mut LocalState, @@ -264,7 +330,8 @@ where // Add indent to measurement if found. if let Some(indent) = self.model.indent(key) { - button_width += f32::from(indent) * f32::from(self.indent_spacing); + button_width = + f32::from(indent).mul_add(f32::from(self.indent_spacing), button_width); } // Add icon to measurement if icon was given. @@ -361,6 +428,7 @@ where self.variant_layout(tree.state.downcast_mut::(), renderer, limits) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, tree: &mut Tree, @@ -376,8 +444,53 @@ where let state = tree.state.downcast_mut::(); if cursor_position.is_over(bounds) { - for (nth, key) in self.model.order.iter().copied().enumerate() { - let bounds = self.variant_button_bounds(bounds, nth); + // 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(Rectangle { + y: bounds.y + 8.0, + width: 16.0, + ..bounds + }) { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + if self.prev_tab_sensitive(state) { + state.buttons_offset -= 1; + } + } + } else { + // Check if the next tab button was clicked. + if cursor_position.is_over(Rectangle { + x: bounds.width, + y: bounds.y + 8.0, + width: 16.0, + ..bounds + }) { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + if self.next_tab_sensitive(state) { + state.buttons_offset += 1; + } + } + } + } + } + + for (nth, key) in self + .model + .order + .iter() + .copied() + .enumerate() + .skip(state.buttons_offset) + .take(state.buttons_visible) + { + let Some(bounds) = self.variant_button_bounds(state, bounds, nth) else { + continue; + }; + if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. @@ -501,7 +614,40 @@ where .. }) = event { - shell.publish(on_activate(state.focused_key)); + match state.focused_item { + Focus::Tab(entity) => { + shell.publish(on_activate(entity)); + } + + Focus::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 = Focus::Tab(first); + } + } + } + } + + Focus::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 = Focus::Tab(last); + } + } + } + } + + Focus::None | Focus::Set => (), + } + return event::Status::Captured; } } @@ -521,21 +667,42 @@ where ) { let state = tree.state.downcast_mut::(); operation.focusable(state, self.id.as_ref().map(|id| &id.0)); + + if let Focus::Set = state.focused_item { + if self.prev_tab_sensitive(state) { + state.focused_item = Focus::PrevButton; + } else if let Some(first) = self.first_tab(state) { + state.focused_item = Focus::Tab(first); + } + } } fn mouse_interaction( &self, - _tree: &Tree, + tree: &Tree, layout: Layout<'_>, cursor_position: mouse::Cursor, _viewport: &iced::Rectangle, _renderer: &Renderer, ) -> iced_core::mouse::Interaction { + let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); if cursor_position.is_over(bounds) { - for (nth, key) in self.model.order.iter().copied().enumerate() { - if cursor_position.is_over(self.variant_button_bounds(bounds, nth)) { + for (nth, key) in self + .model + .order + .iter() + .copied() + .enumerate() + .skip(state.buttons_offset) + .take(state.buttons_visible) + { + let Some(bounds) = self.variant_button_bounds(state, bounds, nth) else { + continue; + }; + + if cursor_position.is_over(bounds) { return if self.model.items[key].enabled { iced_core::mouse::Interaction::Pointer } else { @@ -577,14 +744,106 @@ where ); } + // Draw previous and next tab buttons if there is a need to paginate tabs. + if state.collapsed { + // Previous tab button + let prev_bounds = Rectangle { + y: bounds.y + 8.0, + width: 16.0, + ..bounds + }; + + if let Focus::PrevButton = state.focused_item { + renderer.fill_quad( + renderer::Quad { + bounds: prev_bounds, + border_radius: appearance.focus.first.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + appearance + .focus + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + draw_icon::( + renderer, + theme, + style, + cursor, + viewport, + if state.buttons_offset == 0 { + appearance.inactive.text_color + } else if let Focus::PrevButton = state.focused_item { + appearance.focus.text_color + } else { + appearance.active.text_color + }, + prev_bounds, + icon::from_name("go-previous-symbolic").size(16).icon(), + ); + + // Next tab button + let next_bounds = Rectangle { + x: bounds.width, + y: bounds.y + 8.0, + width: 16.0, + ..bounds + }; + + if let Focus::NextButton = state.focused_item { + renderer.fill_quad( + renderer::Quad { + bounds: next_bounds, + border_radius: appearance.focus.last.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + appearance + .focus + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + draw_icon::( + renderer, + theme, + style, + cursor, + viewport, + if self.next_tab_sensitive(state) { + appearance.active.text_color + } else if let Focus::NextButton = state.focused_item { + appearance.focus.text_color + } else { + appearance.inactive.text_color + }, + next_bounds, + icon::from_name("go-next-symbolic").size(16).icon(), + ); + } + // Draw each of the items in the widget. - for (nth, key) in self.model.order.iter().copied().enumerate() { - let mut bounds = self.variant_button_bounds(bounds, nth); + for (nth, key) in self + .model + .order + .iter() + .copied() + .enumerate() + .skip(state.buttons_offset) + .take(state.buttons_visible) + { + let Some(mut bounds) = self.variant_button_bounds(state, bounds, nth) else { + continue; + }; let key_is_active = self.model.is_active(key); let key_is_hovered = state.hovered == key; - let (status_appearance, font) = if state.focused_key == key { + let (status_appearance, font) = if Focus::Tab(key) == state.focused_item { (appearance.focus, &self.font_active) } else if key_is_active { (appearance.active, &self.font_active) @@ -660,29 +919,19 @@ where let offset = width + f32::from(self.button_spacing); bounds.y = y - width / 2.0; - let mut layout_node = layout::Node::new(Size { - width, - height: width, - }); - - layout_node.move_to(Point { - x: bounds.x, - y: bounds.y, - }); - - Widget::::draw( - Element::::from(icon.clone()).as_widget(), - &Tree::empty(), + draw_icon::( renderer, theme, - &renderer::Style { - icon_color: status_appearance.text_color, - text_color: status_appearance.text_color, - scale_factor: style.scale_factor, - }, - Layout::new(&layout_node), + style, cursor, viewport, + status_appearance.text_color, + Rectangle { + width, + height: width, + ..bounds + }, + icon.clone(), ); bounds.x += offset; @@ -724,7 +973,7 @@ where bounds.position(), status_appearance.text_color, Rectangle { - width: bounds.width - close_icon_width - 16.0, + width: bounds.width - close_icon_width - 12.0, ..original_bounds }, ); @@ -735,29 +984,15 @@ where let close_button_bounds = close_bounds(original_bounds, close_icon_width, self.button_padding); - let mut layout_node = layout::Node::new(Size { - width: close_button_bounds.width, - height: close_button_bounds.height, - }); - - layout_node.move_to(Point { - x: close_button_bounds.x, - y: close_button_bounds.y, - }); - - Widget::::draw( - &Element::::from(self.close_icon.clone()), - &Tree::empty(), + draw_icon::( renderer, theme, - &renderer::Style { - icon_color: status_appearance.text_color, - text_color: status_appearance.text_color, - scale_factor: style.scale_factor, - }, - Layout::new(&layout_node), + style, cursor, viewport, + status_appearance.text_color, + close_button_bounds, + self.close_icon.clone(), ); } } @@ -791,9 +1026,53 @@ where } } -/// A command that focuses a segmented item stored in a widget. -pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id.0)) +/// State that is maintained by each individual widget. +#[derive(Default)] +pub struct LocalState { + /// Whether buttons need to be collapsed to preserve minimum width + pub(super) collapsed: bool, + /// Defines how many buttons to show at a time. + pub(super) buttons_visible: usize, + /// Button visibility offset, when collapsed. + pub(super) buttons_offset: usize, + /// The first focusable key. + first: Entity, + /// If the widget is focused or not. + focused: bool, + /// The key inside the widget that is currently focused. + focused_item: Focus, + /// The ID of the button that is being hovered. Defaults to null. + hovered: Entity, + /// The paragraphs for each text. + paragraphs: SecondaryMap, + /// Time since last tab activation from wheel movements. + wheel_timestamp: Option, +} + +#[derive(Default, PartialEq)] +enum Focus { + NextButton, + #[default] + None, + PrevButton, + Set, + Tab(Entity), +} + +impl operation::Focusable for LocalState { + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self) { + self.focused = true; + self.focused_item = Focus::Set; + } + + fn unfocus(&mut self) { + self.focused = false; + self.focused_item = Focus::None; + } } /// The iced identifier of a segmented button. @@ -832,3 +1111,40 @@ fn close_bounds(area: Rectangle, icon_size: f32, button_padding: [u16; 4]) height: icon_size, } } + +#[allow(clippy::too_many_arguments)] +fn draw_icon( + renderer: &mut Renderer, + theme: &crate::Theme, + style: &renderer::Style, + cursor: mouse::Cursor, + viewport: &Rectangle, + color: Color, + bounds: Rectangle, + icon: Icon, +) { + let mut layout_node = layout::Node::new(Size { + width: bounds.width, + height: bounds.width, + }); + + layout_node.move_to(Point { + x: bounds.x, + y: bounds.y, + }); + + Widget::::draw( + Element::::from(icon.clone()).as_widget(), + &Tree::empty(), + renderer, + theme, + &renderer::Style { + icon_color: color, + text_color: color, + scale_factor: style.scale_factor, + }, + Layout::new(&layout_node), + cursor, + viewport, + ); +} From d6e23fe97751d40f181228d1d1c34b51fea23bea Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 23 Jan 2024 22:40:12 +0100 Subject: [PATCH 0084/1050] fix(segmented_button): text bounds off when an icon is used --- src/widget/segmented_button/widget.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 11b999f7..b56534d0 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -909,15 +909,12 @@ where // Draw the image beside the text. let horizontal_alignment = if let Some(icon) = self.model.icon(key) { bounds.x += f32::from(self.button_padding[0]); - bounds.y += f32::from(self.button_padding[1]); - bounds.width -= - f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); - bounds.height -= - f32::from(self.button_padding[1]) - f32::from(self.button_padding[3]); + let mut image_bounds = bounds; let width = f32::from(icon.size); let offset = width + f32::from(self.button_spacing); - bounds.y = y - width / 2.0; + image_bounds.y += f32::from(self.button_padding[1]); + image_bounds.y = y - width / 2.0; draw_icon::( renderer, @@ -929,7 +926,7 @@ where Rectangle { width, height: width, - ..bounds + ..image_bounds }, icon.clone(), ); @@ -973,7 +970,7 @@ where bounds.position(), status_appearance.text_color, Rectangle { - width: bounds.width - close_icon_width - 12.0, + width: bounds.width - close_icon_width, ..original_bounds }, ); From 912e8b0a4478e67ad4b3ce46e327c70dbe9887d8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 24 Jan 2024 11:27:03 -0500 Subject: [PATCH 0085/1050] chore: update freedesktop-icons --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bd510142..45cb43d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,7 @@ url = "2.4.0" zbus = {version = "3.14.1", default-features = false, optional = true} [target.'cfg(unix)'.dependencies] -freedesktop-icons = "0.2.4" +freedesktop-icons = "0.2.5" [dependencies.cosmic-theme] path = "cosmic-theme" From 3aef16bf9ed7f0b2ffbaa46b0d60b1a663ebcbd7 Mon Sep 17 00:00:00 2001 From: Gary Guo Date: Tue, 26 Dec 2023 22:09:53 +0000 Subject: [PATCH 0086/1050] improv(cosmic-config): remove hardcoded paths This commit changes the hardcoded /usr/share paths in cosmic-config to become performed via XDG lookups using the `xdg` crate. This allows the installed files to be discovered on non-FHS Linux, e.g. NixOS. Hardcoded /var/lib/ is removed entirely because 1. nothing installs to it yet (only user of new_state is cosmic_bg currently and it does not install to /var/lib) 2. it's intended for system states, not template for user state. 3. it's not part of XDG spec. On Windows the known folder crate is used. Signed-off-by: Gary Guo --- cosmic-config/Cargo.toml | 7 ++- cosmic-config/src/lib.rs | 111 +++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 154183e4..68af584e 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -10,7 +10,6 @@ macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] -# For redox support zbus = { version = "3.14.1", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.12.2", optional = true } @@ -24,3 +23,9 @@ iced_futures = { path = "../iced/futures/", default-features = false, optional = once_cell = "1.19.0" cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", branch = "cosmic-settings-daemon", optional = true } futures-util = { version = "0.3", optional = true } + +[target.'cfg(unix)'.dependencies] +xdg = "2.1" + +[target.'cfg(windows)'.dependencies] +known-folders = "1.1.0" diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index d6a4072b..ef9c514a 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -94,10 +94,23 @@ pub trait ConfigSet { #[derive(Clone, Debug)] pub struct Config { - system_path: PathBuf, + system_path: Option, user_path: PathBuf, } +/// Check that the name is relative and doesn't contain . or .. +fn sanitize_name(name: &str) -> Result<&Path, Error> { + let path = Path::new(name); + if path + .components() + .all(|x| matches!(x, std::path::Component::Normal(_))) + { + Ok(path) + } else { + Err(Error::InvalidName(name.to_owned())) + } +} + impl Config { /// Get the config for the libcosmic toolkit pub fn libcosmic() -> Result { @@ -108,33 +121,34 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new(name: &str, version: u64) -> Result { - // Get libcosmic system defaults path - //TODO: support non-UNIX OS - let cosmic_system_path = Path::new("/usr/share/cosmic"); - // Append [name]/v[version] - let system_path = cosmic_system_path.join(name).join(format!("v{}", version)); + // Look for [name]/v[version] + let path = sanitize_name(name)?.join(format!("v{}", version)); + + // Search data file, which provides default (e.g. /usr/share) + #[cfg(unix)] + let system_path = xdg::BaseDirectories::with_prefix("cosmic") + .map_err(std::io::Error::from)? + .find_data_file(&path); + + #[cfg(windows)] + let system_path = + known_folders::get_known_folder_path(known_folders::KnownFolder::ProgramFilesCommon) + .map(|x| x.join("COSMIC").join(&path)); // Get libcosmic user configuration directory let cosmic_user_path = dirs::config_dir() .ok_or(Error::NoConfigDirectory)? .join("cosmic"); - // Append [name]/v[version] - let user_path = cosmic_user_path.join(name).join(format!("v{}", version)); - // If the app paths are children of the cosmic paths - if system_path.starts_with(&cosmic_system_path) && user_path.starts_with(&cosmic_user_path) - { - // Create app user path - fs::create_dir_all(&user_path)?; - // Return Config - Ok(Self { - system_path, - user_path, - }) - } else { - // Return error for invalid name - Err(Error::InvalidName(name.to_string())) - } + let user_path = cosmic_user_path.join(path); + // Create new configuration directory if not found. + fs::create_dir_all(&user_path)?; + + // Return Config + Ok(Self { + system_path, + user_path, + }) } /// Get state for the given application name and config version. State is meant to be used to @@ -143,33 +157,22 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new_state(name: &str, version: u64) -> Result { - // Get libcosmic system defaults path - //TODO: support non-UNIX OS - let cosmic_system_path = Path::new("/var/lib/cosmic"); - // Append [name]/v[version] - let system_path = cosmic_system_path.join(name).join(format!("v{}", version)); + // Look for [name]/v[version] + let path = sanitize_name(name)?.join(format!("v{}", version)); - // Get libcosmic user configuration directory + // Get libcosmic user state directory let cosmic_user_path = dirs::state_dir() .ok_or(Error::NoConfigDirectory)? .join("cosmic"); - // Append [name]/v[version] - let user_path = cosmic_user_path.join(name).join(format!("v{}", version)); - // If the app paths are children of the cosmic paths - if system_path.starts_with(&cosmic_system_path) && user_path.starts_with(&cosmic_user_path) - { - // Create app user path - fs::create_dir_all(&user_path)?; - // Return Config - Ok(Self { - system_path, - user_path, - }) - } else { - // Return error for invalid name - Err(Error::InvalidName(name.to_string())) - } + let user_path = cosmic_user_path.join(path); + // Create new state directory if not found. + fs::create_dir_all(&user_path)?; + + Ok(Self { + system_path: None, + user_path, + }) } // Start a transaction (to set multiple configs at the same time) @@ -238,23 +241,15 @@ impl Config { } fn default_path(&self, key: &str) -> Result { - let default_path = self.system_path.join(key); - // Ensure key path is a direct child of config directory - if default_path.parent() == Some(&self.system_path) { - Ok(default_path) - } else { - Err(Error::InvalidName(key.to_string())) - } + let Some(system_path) = self.system_path.as_ref() else { + return Err(Error::NoConfigDirectory); + }; + + Ok(system_path.join(sanitize_name(key)?)) } fn key_path(&self, key: &str) -> Result { - let key_path = self.user_path.join(key); - // Ensure key path is a direct child of config directory - if key_path.parent() == Some(&self.user_path) { - Ok(key_path) - } else { - Err(Error::InvalidName(key.to_string())) - } + Ok(self.user_path.join(sanitize_name(key)?)) } } From d5b2a2e87cacf83843fc676cabeae7f84a8a8774 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 24 Jan 2024 17:32:50 +0100 Subject: [PATCH 0087/1050] feat(segmented_button): variable-width horizontal button when width is `Shrink` --- src/widget/segmented_button/horizontal.rs | 150 +++++++++++++----- src/widget/segmented_button/vertical.rs | 31 ++-- src/widget/segmented_button/widget.rs | 181 ++++++++++------------ 3 files changed, 213 insertions(+), 149 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 7b8b374a..b15e6383 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -3,12 +3,13 @@ //! Implementation details for the horizontal layout of a segmented button. -use super::model::{Model, Selectable}; +use super::model::{Entity, Model, Selectable}; use super::style::StyleSheet; use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_core::layout; +use iced_core::text::Renderer; /// Horizontal [`SegmentedButton`]. pub type HorizontalSegmentedButton<'a, SelectionMode, Message> = @@ -48,34 +49,40 @@ where &self, state: &LocalState, mut bounds: Rectangle, - nth: usize, - ) -> Option { + ) -> impl Iterator { let num = state.buttons_visible; + let spacing = f32::from(self.spacing); + let mut homogenous_width = 0.0; - // Do not display tabs that are currently hidden due to width constraints. - if state.collapsed && nth < state.buttons_offset { - return None; - } - - if num != 0 { - let offset_width; - (bounds.x, offset_width) = if state.collapsed { - (bounds.x + 16.0, 32.0) - } else { - (bounds.x, 0.0) - }; - - let spacing = f32::from(self.spacing); - bounds.width = ((num as f32).mul_add(-spacing, bounds.width - offset_width) + spacing) - / num as f32; - - if nth != state.buttons_offset { - let pos = (nth - state.buttons_offset) as f32; - bounds.x += pos.mul_add(bounds.width, pos * spacing); + if Length::Shrink != self.width || state.collapsed { + if state.collapsed { + bounds.x += 16.0; + bounds.width -= 32.0; } + + homogenous_width = + ((num as f32).mul_add(-spacing, bounds.width) + spacing) / num as f32; } - Some(bounds) + self.model + .order + .iter() + .copied() + .enumerate() + .skip(state.buttons_offset) + .take(state.buttons_visible) + .map(move |(nth, key)| { + let mut this_bounds = bounds; + + if !state.collapsed && Length::Shrink == self.width { + this_bounds.width = state.internal_layout[nth].width; + } else { + this_bounds.width = homogenous_width; + } + + bounds.x += this_bounds.width + spacing; + (key, this_bounds) + }) } #[allow(clippy::cast_precision_loss)] @@ -87,27 +94,92 @@ where renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits.width(self.width); - let (mut width, height) = self.max_button_dimensions(state, renderer, limits.max()); - - let num = self.model.items.len(); + let num = self.model.order.len(); + let mut total_width = 0.0; let spacing = f32::from(self.spacing); + let limits = limits.width(self.width); + let size; - if num != 0 { - width = (num as f32).mul_add(width, num as f32 * spacing) - spacing; + if state.known_length != num { + if state.known_length > num { + state.buttons_offset -= state.buttons_offset.min(state.known_length - num); + } else { + state.buttons_offset += num - state.known_length; + } + + state.known_length = num; } - let size = limits - .height(Length::Fixed(height)) - .resolve(Size::new(width, height)); + if let Length::Shrink = self.width { + // Buttons will be rendered at their smallest widths possible. + state.internal_layout.clear(); - let actual_width = size.width as usize; - let minimum_width = self.minimum_button_width as usize * self.model.items.len(); + let font = renderer.default_font(); + let mut total_height = 0.0f32; - state.buttons_visible = num; - state.collapsed = actual_width < minimum_width; - if state.collapsed { - state.buttons_visible = (actual_width / self.minimum_button_width as usize).min(num); + for &button in &self.model.order { + let (mut width, height) = self.button_dimensions(state, font, button); + width = f32::from(self.minimum_button_width).max(width); + total_width += width + spacing; + total_height = total_height.max(height); + + state.internal_layout.push(Size::new(width, height)); + } + + // Get the max available width for placing buttons into. + let max_size = limits + .height(Length::Fixed(total_height)) + .resolve(Size::new(f32::MAX, total_height)); + + let mut visible_width = 32.0; + state.buttons_visible = 0; + + for button_size in &state.internal_layout { + visible_width += button_size.width; + + if max_size.width >= visible_width { + state.buttons_visible += 1; + } else { + break; + } + + visible_width += spacing; + } + + state.collapsed = num > 1 && state.buttons_visible != num; + + // If collapsed, use the maximum width available. + visible_width = if state.collapsed { + max_size.width - 32.0 + } else { + total_width + }; + + size = limits + .height(Length::Fixed(total_height)) + .resolve(Size::new(visible_width, total_height)); + } else { + // Buttons will be rendered with equal widths. + state.buttons_visible = self.model.items.len(); + let (width, height) = self.max_button_dimensions(state, renderer, limits.max()); + let total_width = (state.buttons_visible as f32) * (width + spacing); + + size = limits + .height(Length::Fixed(height)) + .resolve(Size::new(total_width, height)); + + let actual_width = size.width as usize; + let minimum_width = state.buttons_visible * self.minimum_button_width as usize; + state.collapsed = actual_width < minimum_width; + + if state.collapsed { + state.buttons_visible = + (actual_width / self.minimum_button_width as usize).min(state.buttons_visible); + } + } + + if !state.collapsed { + state.buttons_offset = 0; } layout::Node::new(size) diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index c6c6b077..5c541a85 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -3,7 +3,7 @@ //! Implementation details for the vertical layout of a segmented button. -use super::model::{Model, Selectable}; +use super::model::{Entity, Model, Selectable}; use super::style::StyleSheet; use super::widget::{LocalState, SegmentedButton, SegmentedVariant}; @@ -47,21 +47,22 @@ where #[allow(clippy::cast_precision_loss)] fn variant_button_bounds( &self, - _state: &LocalState, + state: &LocalState, mut bounds: Rectangle, - nth: usize, - ) -> Option { - let num = self.model.items.len(); - if num != 0 { - let spacing = f32::from(self.spacing); - bounds.height = (bounds.height - (num as f32 * spacing) + spacing) / num as f32; + ) -> impl Iterator { + let spacing = f32::from(self.spacing); - if nth != 0 { - bounds.y += (nth as f32 * bounds.height) + (nth as f32 * spacing); - } - } - - Some(bounds) + self.model + .order + .iter() + .copied() + .enumerate() + .map(move |(_nth, key)| { + let mut this_bounds = bounds; + this_bounds.height = state.internal_layout[0].height; + bounds.y += this_bounds.height + spacing; + (key, this_bounds) + }) } #[allow(clippy::cast_precision_loss)] @@ -73,8 +74,10 @@ where renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { + state.internal_layout.clear(); let limits = limits.width(self.width); let (width, mut height) = self.max_button_dimensions(state, renderer, limits.max()); + state.internal_layout.push(Size::new(width, height)); let num = self.model.items.len(); let spacing = f32::from(self.spacing); diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index b56534d0..63214ddf 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -32,13 +32,12 @@ pub trait SegmentedVariant { style: &crate::theme::SegmentedButton, ) -> super::Appearance; - /// Calculates the bounds for the given button by its position. + /// Calculates the bounds for visible buttons. fn variant_button_bounds( &self, state: &LocalState, bounds: Rectangle, - position: usize, - ) -> Option; + ) -> impl Iterator; /// Calculates the layout of this variant. fn variant_layout( @@ -137,6 +136,7 @@ where } } + /// Emitted when a tab is pressed. pub fn on_activate(mut self, on_activate: T) -> Self where T: Fn(Entity) -> Message + 'static, @@ -145,6 +145,7 @@ where self } + /// Emitted when a tab close button is pressed. pub fn on_close(mut self, on_close: T) -> Self where T: Fn(Entity) -> Message + 'static, @@ -293,6 +294,65 @@ where state.buttons_offset < self.model.order.len() - state.buttons_visible } + pub(super) fn button_dimensions( + &self, + state: &mut LocalState, + font: crate::font::Font, + button: Entity, + ) -> (f32, f32) { + let mut width = 0.0f32; + let mut height = 0.0f32; + + // Add text to measurement if text was given. + if let Some((text, entry)) = self + .model + .text + .get(button) + .zip(state.paragraphs.entry(button)) + { + let paragraph = entry.or_insert_with(|| { + crate::Paragraph::with_text(Text { + content: text, + size: iced::Pixels(self.font_size), + bounds: Size::INFINITY, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: Shaping::Advanced, + line_height: self.line_height, + }) + }); + + let size = paragraph.min_bounds(); + width += size.width; + height += size.height; + } + + // Add indent to measurement if found. + if let Some(indent) = self.model.indent(button) { + width = f32::from(indent).mul_add(f32::from(self.indent_spacing), width); + } + + // Add icon to measurement if icon was given. + if let Some(icon) = self.model.icon(button) { + height = height.max(f32::from(icon.size)); + width += f32::from(icon.size) + f32::from(self.button_spacing); + } + + // Add close button to measurement if found. + if self.model.is_closable(button) { + height = height.max(f32::from(self.close_icon.size)); + width += f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0; + } + + // Add button padding to the max size found + width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); + height += f32::from(self.button_padding[1]) + f32::from(self.button_padding[3]); + height = height.max(f32::from(self.button_height)); + + (width, height) + } + pub(super) fn max_button_dimensions( &self, state: &mut LocalState, @@ -304,58 +364,12 @@ where let font = renderer.default_font(); for key in self.model.order.iter().copied() { - let mut button_width = 0.0f32; - let mut button_height = 0.0f32; - - // Add text to measurement if text was given. - if let Some((text, entry)) = self.model.text.get(key).zip(state.paragraphs.entry(key)) { - let paragraph = entry.or_insert_with(|| { - crate::Paragraph::with_text(Text { - content: text, - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - line_height: self.line_height, - }) - }); - - let Size { width, height } = paragraph.min_bounds(); - - button_width = width; - button_height = height; - } - - // Add indent to measurement if found. - if let Some(indent) = self.model.indent(key) { - button_width = - f32::from(indent).mul_add(f32::from(self.indent_spacing), button_width); - } - - // Add icon to measurement if icon was given. - if let Some(icon) = self.model.icon(key) { - button_height = button_height.max(f32::from(icon.size)); - button_width += f32::from(icon.size) + f32::from(self.button_spacing); - } - - // Add close button to measurement if found. - if self.model.is_closable(key) { - button_height = button_height.max(f32::from(self.close_icon.size)); - button_width += - f32::from(self.close_icon.size) + f32::from(self.button_spacing) + 8.0; - } + let (button_width, button_height) = self.button_dimensions(state, font, key); height = height.max(button_height); width = width.max(button_width); } - // Add button padding to the max size found - width += f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); - height += f32::from(self.button_padding[1]) + f32::from(self.button_padding[3]); - height = height.max(f32::from(self.button_height)); - (width, height) } } @@ -373,9 +387,7 @@ where } fn state(&self) -> tree::State { - // update the paragraphs for the model tree::State::new(LocalState { - first: self.model.order.iter().copied().next().unwrap_or_default(), paragraphs: SecondaryMap::new(), ..LocalState::default() }) @@ -478,19 +490,10 @@ where } } - for (nth, key) in self - .model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) + for (key, bounds) in self + .variant_button_bounds(state, bounds) + .collect::>() { - let Some(bounds) = self.variant_button_bounds(state, bounds, nth) else { - continue; - }; - if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. @@ -689,19 +692,7 @@ where let bounds = layout.bounds(); if cursor_position.is_over(bounds) { - for (nth, key) in self - .model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) - { - let Some(bounds) = self.variant_button_bounds(state, bounds, nth) else { - continue; - }; - + for (key, bounds) in self.variant_button_bounds(state, bounds) { if cursor_position.is_over(bounds) { return if self.model.items[key].enabled { iced_core::mouse::Interaction::Pointer @@ -827,19 +818,7 @@ where } // Draw each of the items in the widget. - for (nth, key) in self - .model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) - { - let Some(mut bounds) = self.variant_button_bounds(state, bounds, nth) else { - continue; - }; - + for (nth, (key, mut bounds)) in self.variant_button_bounds(state, bounds).enumerate() { let key_is_active = self.model.is_active(key); let key_is_hovered = state.hovered == key; @@ -970,7 +949,15 @@ where bounds.position(), status_appearance.text_color, Rectangle { - width: bounds.width - close_icon_width, + width: { + let width = bounds.width - close_icon_width; + // TODO: determine cause of differences here. + if self.model.icon(key).is_some() { + width - f32::from(self.button_spacing) + } else { + width - 12.0 + } + }, ..original_bounds }, ); @@ -1026,20 +1013,22 @@ where /// State that is maintained by each individual widget. #[derive(Default)] pub struct LocalState { - /// Whether buttons need to be collapsed to preserve minimum width - pub(super) collapsed: bool, /// Defines how many buttons to show at a time. pub(super) buttons_visible: usize, /// Button visibility offset, when collapsed. pub(super) buttons_offset: usize, - /// The first focusable key. - first: Entity, + /// Whether buttons need to be collapsed to preserve minimum width + pub(super) collapsed: bool, /// If the widget is focused or not. focused: bool, /// The key inside the widget that is currently focused. focused_item: Focus, /// The ID of the button that is being hovered. Defaults to null. hovered: Entity, + /// Last known length of the model. + pub(super) known_length: usize, + /// Dimensions of internal buttons when shrinking + pub(super) internal_layout: Vec, /// The paragraphs for each text. paragraphs: SecondaryMap, /// Time since last tab activation from wheel movements. @@ -1131,7 +1120,7 @@ fn draw_icon( }); Widget::::draw( - Element::::from(icon.clone()).as_widget(), + Element::::from(icon).as_widget(), &Tree::empty(), renderer, theme, From 135770a16d928e42601420bddfd9056e9300eb26 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 26 Jan 2024 20:30:23 +0100 Subject: [PATCH 0088/1050] fix(segmented_button): hoverable tab pagination buttons; fixed padding --- src/widget/segmented_button/horizontal.rs | 21 ++- src/widget/segmented_button/widget.rs | 192 +++++++++++++--------- 2 files changed, 128 insertions(+), 85 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index b15e6383..f308a949 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -55,13 +55,15 @@ where let mut homogenous_width = 0.0; if Length::Shrink != self.width || state.collapsed { + let mut width_offset = 0.0; if state.collapsed { - bounds.x += 16.0; - bounds.width -= 32.0; + bounds.x += f32::from(self.button_height); + width_offset = f32::from(self.button_height) * 2.0; } - homogenous_width = - ((num as f32).mul_add(-spacing, bounds.width) + spacing) / num as f32; + homogenous_width = ((num as f32).mul_add(-spacing, bounds.width - width_offset) + + spacing) + / num as f32; } self.model @@ -98,7 +100,7 @@ where let mut total_width = 0.0; let spacing = f32::from(self.spacing); let limits = limits.width(self.width); - let size; + let mut size; if state.known_length != num { if state.known_length > num { @@ -131,7 +133,7 @@ where .height(Length::Fixed(total_height)) .resolve(Size::new(f32::MAX, total_height)); - let mut visible_width = 32.0; + let mut visible_width = f32::from(self.button_height) * 2.0; state.buttons_visible = 0; for button_size in &state.internal_layout { @@ -150,12 +152,13 @@ where // If collapsed, use the maximum width available. visible_width = if state.collapsed { - max_size.width - 32.0 + max_size.width - f32::from(self.button_height) } else { total_width }; size = limits + .width(Length::Fixed(visible_width)) .height(Length::Fixed(total_height)) .resolve(Size::new(visible_width, total_height)); } else { @@ -173,6 +176,10 @@ where state.collapsed = actual_width < minimum_width; if state.collapsed { + size = limits + .height(Length::Fixed(height)) + .resolve(Size::new(f32::MAX, height)); + state.buttons_visible = (actual_width / self.minimum_button_width as usize).min(state.buttons_visible); } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 63214ddf..071ad562 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -159,10 +159,10 @@ where self.model.items.get(key).map_or(false, |item| item.enabled) } - /// Focus the previous item in the widget. + /// Item the previous item in the widget. fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { match state.focused_item { - Focus::Tab(entity) => { + Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state).rev(); while let Some(key) = keys.next() { @@ -173,7 +173,7 @@ where continue; } - state.focused_item = Focus::Tab(key); + state.focused_item = Item::Tab(key); return event::Status::Captured; } @@ -182,39 +182,39 @@ where } if self.prev_tab_sensitive(state) { - state.focused_item = Focus::PrevButton; + state.focused_item = Item::PrevButton; return event::Status::Captured; } } - Focus::NextButton => { + Item::NextButton => { if let Some(last) = self.last_tab(state) { - state.focused_item = Focus::Tab(last); + state.focused_item = Item::Tab(last); return event::Status::Captured; } } - Focus::None => { + Item::None => { if self.next_tab_sensitive(state) { - state.focused_item = Focus::NextButton; + state.focused_item = Item::NextButton; return event::Status::Captured; } else if let Some(last) = self.last_tab(state) { - state.focused_item = Focus::Tab(last); + state.focused_item = Item::Tab(last); return event::Status::Captured; } } - Focus::PrevButton | Focus::Set => (), + Item::PrevButton | Item::Set => (), } - state.focused_item = Focus::None; + state.focused_item = Item::None; event::Status::Ignored } - /// Focus the next item in the widget. + /// Item the next item in the widget. fn focus_next(&mut self, state: &mut LocalState) -> event::Status { match state.focused_item { - Focus::Tab(entity) => { + Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state); while let Some(key) = keys.next() { if key == entity { @@ -224,7 +224,7 @@ where continue; } - state.focused_item = Focus::Tab(key); + state.focused_item = Item::Tab(key); return event::Status::Captured; } @@ -233,32 +233,32 @@ where } if self.next_tab_sensitive(state) { - state.focused_item = Focus::NextButton; + state.focused_item = Item::NextButton; return event::Status::Captured; } } - Focus::PrevButton => { + Item::PrevButton => { if let Some(first) = self.first_tab(state) { - state.focused_item = Focus::Tab(first); + state.focused_item = Item::Tab(first); return event::Status::Captured; } } - Focus::None => { + Item::None => { if self.prev_tab_sensitive(state) { - state.focused_item = Focus::PrevButton; + state.focused_item = Item::PrevButton; return event::Status::Captured; } else if let Some(first) = self.first_tab(state) { - state.focused_item = Focus::Tab(first); + state.focused_item = Item::Tab(first); return event::Status::Captured; } } - Focus::NextButton | Focus::Set => (), + Item::NextButton | Item::Set => (), } - state.focused_item = Focus::None; + state.focused_item = Item::None; event::Status::Ignored } @@ -460,31 +460,33 @@ where if state.collapsed { // Check if the prev tab button was clicked. if cursor_position.is_over(Rectangle { - y: bounds.y + 8.0, - width: 16.0, - ..bounds - }) { + x: bounds.x, + y: bounds.y, + width: f32::from(self.button_height), + height: f32::from(self.button_height), + }) && self.prev_tab_sensitive(state) + { + state.hovered = Item::PrevButton; if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { - if self.prev_tab_sensitive(state) { - state.buttons_offset -= 1; - } + state.buttons_offset -= 1; } } else { // Check if the next tab button was clicked. if cursor_position.is_over(Rectangle { - x: bounds.width, - y: bounds.y + 8.0, - width: 16.0, - ..bounds - }) { + x: bounds.width - f32::from(self.button_height) / 4.0 - 8.0, + y: bounds.y, + width: f32::from(self.button_height), + height: f32::from(self.button_height), + }) && self.next_tab_sensitive(state) + { + state.hovered = Item::NextButton; + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { - if self.next_tab_sensitive(state) { - state.buttons_offset += 1; - } + state.buttons_offset += 1; } } } @@ -497,7 +499,7 @@ where if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. - state.hovered = key; + state.hovered = Item::Tab(key); // If marked as closable, show a close icon. if self.model.items[key].closable { @@ -594,7 +596,7 @@ where } } } else { - state.hovered = Entity::default(); + state.hovered = Item::None; } if state.focused { @@ -618,37 +620,37 @@ where }) = event { match state.focused_item { - Focus::Tab(entity) => { + Item::Tab(entity) => { shell.publish(on_activate(entity)); } - Focus::PrevButton => { + 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 = Focus::Tab(first); + state.focused_item = Item::Tab(first); } } } } - Focus::NextButton => { + 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 = Focus::Tab(last); + state.focused_item = Item::Tab(last); } } } } - Focus::None | Focus::Set => (), + Item::None | Item::Set => (), } return event::Status::Captured; @@ -671,11 +673,11 @@ where let state = tree.state.downcast_mut::(); operation.focusable(state, self.id.as_ref().map(|id| &id.0)); - if let Focus::Set = state.focused_item { + if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { - state.focused_item = Focus::PrevButton; + state.focused_item = Item::PrevButton; } else if let Some(first) = self.first_tab(state) { - state.focused_item = Focus::Tab(first); + state.focused_item = Item::Tab(first); } } } @@ -717,6 +719,7 @@ where cursor: mouse::Cursor, viewport: &iced::Rectangle, ) { + let cosmic_theme = theme.cosmic(); let state = tree.state.downcast_ref::(); let appearance = Self::variant_appearance(theme, &self.style); let bounds = layout.bounds(); @@ -738,22 +741,28 @@ where // Draw previous and next tab buttons if there is a need to paginate tabs. if state.collapsed { // Previous tab button - let prev_bounds = Rectangle { - y: bounds.y + 8.0, - width: 16.0, - ..bounds + let mut background_appearance = if Item::PrevButton == state.focused_item { + Some(appearance.focus) + } else if Item::PrevButton == state.hovered { + Some(appearance.hover) + } else { + None }; - if let Focus::PrevButton = state.focused_item { + if let Some(background_appearance) = background_appearance.take() { renderer.fill_quad( renderer::Quad { - bounds: prev_bounds, - border_radius: appearance.focus.first.border_radius, + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: f32::from(self.button_height), + height: bounds.height, + }, + border_radius: cosmic_theme.radius_s().into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, - appearance - .focus + background_appearance .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); @@ -767,33 +776,49 @@ where viewport, if state.buttons_offset == 0 { appearance.inactive.text_color - } else if let Focus::PrevButton = state.focused_item { + } else if let Item::PrevButton = state.focused_item { appearance.focus.text_color } else { appearance.active.text_color }, - prev_bounds, + Rectangle { + x: bounds.x + f32::from(self.button_height) / 4.0, + y: bounds.y + f32::from(self.button_height) / 4.0, + width: 16.0, + height: 16.0, + }, icon::from_name("go-previous-symbolic").size(16).icon(), ); // Next tab button - let next_bounds = Rectangle { - x: bounds.width, - y: bounds.y + 8.0, - width: 16.0, - ..bounds + background_appearance = if Item::NextButton == state.focused_item { + Some(appearance.focus) + } else if Item::NextButton == state.hovered { + Some(appearance.hover) + } else { + None }; - if let Focus::NextButton = state.focused_item { + if let Some(background_appearance) = background_appearance { renderer.fill_quad( renderer::Quad { - bounds: next_bounds, - border_radius: appearance.focus.last.border_radius, + bounds: Rectangle { + x: bounds.width + - f32::from(self.button_height) / 2.0 + - if let Length::Shrink = self.width { + 0.0 + } else { + 8.0 + }, + y: bounds.y, + width: f32::from(self.button_height), + height: bounds.height, + }, + border_radius: cosmic_theme.radius_s().into(), border_width: 0.0, border_color: Color::TRANSPARENT, }, - appearance - .focus + background_appearance .background .unwrap_or(Background::Color(Color::TRANSPARENT)), ); @@ -807,12 +832,23 @@ where viewport, if self.next_tab_sensitive(state) { appearance.active.text_color - } else if let Focus::NextButton = state.focused_item { + } else if let Item::NextButton = state.focused_item { appearance.focus.text_color } else { appearance.inactive.text_color }, - next_bounds, + Rectangle { + x: bounds.width + - f32::from(self.button_height) / 4.0 + - if let Length::Shrink = self.width { + 0.0 + } else { + 8.0 + }, + y: bounds.y + f32::from(self.button_height) / 4.0, + width: 16.0, + height: 16.0, + }, icon::from_name("go-next-symbolic").size(16).icon(), ); } @@ -820,9 +856,9 @@ where // Draw each of the items in the widget. for (nth, (key, mut bounds)) in self.variant_button_bounds(state, bounds).enumerate() { let key_is_active = self.model.is_active(key); - let key_is_hovered = state.hovered == key; + let key_is_hovered = state.hovered == Item::Tab(key); - let (status_appearance, font) = if Focus::Tab(key) == state.focused_item { + let (status_appearance, font) = if Item::Tab(key) == state.focused_item { (appearance.focus, &self.font_active) } else if key_is_active { (appearance.active, &self.font_active) @@ -1022,9 +1058,9 @@ pub struct LocalState { /// If the widget is focused or not. focused: bool, /// The key inside the widget that is currently focused. - focused_item: Focus, + focused_item: Item, /// The ID of the button that is being hovered. Defaults to null. - hovered: Entity, + hovered: Item, /// Last known length of the model. pub(super) known_length: usize, /// Dimensions of internal buttons when shrinking @@ -1036,7 +1072,7 @@ pub struct LocalState { } #[derive(Default, PartialEq)] -enum Focus { +enum Item { NextButton, #[default] None, @@ -1052,12 +1088,12 @@ impl operation::Focusable for LocalState { fn focus(&mut self) { self.focused = true; - self.focused_item = Focus::Set; + self.focused_item = Item::Set; } fn unfocus(&mut self) { self.focused = false; - self.focused_item = Focus::None; + self.focused_item = Item::None; } } From 213ede371b8f37780267542eb2e1af09ca0173fc Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Sat, 27 Jan 2024 02:38:07 +0100 Subject: [PATCH 0089/1050] fix(segmented_button): misaligned next tab button --- src/widget/segmented_button/horizontal.rs | 2 +- src/widget/segmented_button/widget.rs | 19 ++++--------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index f308a949..9ba58022 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -152,7 +152,7 @@ where // If collapsed, use the maximum width available. visible_width = if state.collapsed { - max_size.width - f32::from(self.button_height) + max_size.width } else { total_width }; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 071ad562..a223525a 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -475,7 +475,7 @@ where } else { // Check if the next tab button was clicked. if cursor_position.is_over(Rectangle { - x: bounds.width - f32::from(self.button_height) / 4.0 - 8.0, + x: bounds.x + bounds.width - f32::from(self.button_height), y: bounds.y, width: f32::from(self.button_height), height: f32::from(self.button_height), @@ -803,13 +803,7 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.width - - f32::from(self.button_height) / 2.0 - - if let Length::Shrink = self.width { - 0.0 - } else { - 8.0 - }, + x: bounds.x + bounds.width - f32::from(self.button_height), y: bounds.y, width: f32::from(self.button_height), height: bounds.height, @@ -838,13 +832,8 @@ where appearance.inactive.text_color }, Rectangle { - x: bounds.width - - f32::from(self.button_height) / 4.0 - - if let Length::Shrink = self.width { - 0.0 - } else { - 8.0 - }, + x: bounds.x + bounds.width - f32::from(self.button_height) + + f32::from(self.button_height) / 4.0, y: bounds.y + f32::from(self.button_height) / 4.0, width: 16.0, height: 16.0, From 1291a48d4d62f1da5ca178292a3ce0082204d8d4 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 29 Jan 2024 09:43:06 -0700 Subject: [PATCH 0090/1050] Set MSRV to 1.71 --- Cargo.toml | 1 + src/widget/segmented_button/horizontal.rs | 44 ++++++++++++----------- src/widget/segmented_button/vertical.rs | 32 +++++++++-------- src/widget/segmented_button/widget.rs | 8 ++--- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45cb43d5..cbe64aca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "libcosmic" version = "0.1.0" edition = "2021" +rust-version = "1.71" [lib] name = "cosmic" diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 9ba58022..5fb02916 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -45,11 +45,11 @@ where } #[allow(clippy::cast_precision_loss)] - fn variant_button_bounds( - &self, - state: &LocalState, + fn variant_button_bounds<'b>( + &'b self, + state: &'b LocalState, mut bounds: Rectangle, - ) -> impl Iterator { + ) -> Box + 'b> { let num = state.buttons_visible; let spacing = f32::from(self.spacing); let mut homogenous_width = 0.0; @@ -66,25 +66,27 @@ where / num as f32; } - self.model - .order - .iter() - .copied() - .enumerate() - .skip(state.buttons_offset) - .take(state.buttons_visible) - .map(move |(nth, key)| { - let mut this_bounds = bounds; + Box::new( + self.model + .order + .iter() + .copied() + .enumerate() + .skip(state.buttons_offset) + .take(state.buttons_visible) + .map(move |(nth, key)| { + let mut this_bounds = bounds; - if !state.collapsed && Length::Shrink == self.width { - this_bounds.width = state.internal_layout[nth].width; - } else { - this_bounds.width = homogenous_width; - } + if !state.collapsed && Length::Shrink == self.width { + this_bounds.width = state.internal_layout[nth].width; + } else { + this_bounds.width = homogenous_width; + } - bounds.x += this_bounds.width + spacing; - (key, this_bounds) - }) + bounds.x += this_bounds.width + spacing; + (key, this_bounds) + }), + ) } #[allow(clippy::cast_precision_loss)] diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 5c541a85..c9fc5833 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -45,24 +45,26 @@ where } #[allow(clippy::cast_precision_loss)] - fn variant_button_bounds( - &self, - state: &LocalState, + fn variant_button_bounds<'b>( + &'b self, + state: &'b LocalState, mut bounds: Rectangle, - ) -> impl Iterator { + ) -> Box + 'b> { let spacing = f32::from(self.spacing); - self.model - .order - .iter() - .copied() - .enumerate() - .map(move |(_nth, key)| { - let mut this_bounds = bounds; - this_bounds.height = state.internal_layout[0].height; - bounds.y += this_bounds.height + spacing; - (key, this_bounds) - }) + Box::new( + self.model + .order + .iter() + .copied() + .enumerate() + .map(move |(_nth, key)| { + let mut this_bounds = bounds; + this_bounds.height = state.internal_layout[0].height; + bounds.y += this_bounds.height + spacing; + (key, this_bounds) + }), + ) } #[allow(clippy::cast_precision_loss)] diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index a223525a..f2d58d88 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -33,11 +33,11 @@ pub trait SegmentedVariant { ) -> super::Appearance; /// Calculates the bounds for visible buttons. - fn variant_button_bounds( - &self, - state: &LocalState, + fn variant_button_bounds<'b>( + &'b self, + state: &'b LocalState, bounds: Rectangle, - ) -> impl Iterator; + ) -> Box + 'b>; /// Calculates the layout of this variant. fn variant_layout( From bf0508816b7e7098cfcc6eb16ee288207ef0cc31 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Thu, 25 Jan 2024 15:02:31 +0000 Subject: [PATCH 0091/1050] libcosmic: Add desktop-file helpers --- Cargo.toml | 5 +- src/desktop.rs | 213 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/desktop.rs diff --git a/Cargo.toml b/Cargo.toml index cbe64aca..3e8929df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ pipewire = ["ashpd?/pipewire"] process = ["dep:nix"] # Use rfd for file dialogs rfd = ["dep:rfd"] +# Enables desktop files helpers +desktop = ["process", "dep:freedesktop-desktop-entry", "dep:shlex"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # Prevents multiple separate process instances. @@ -79,6 +81,8 @@ zbus = {version = "3.14.1", default-features = false, optional = true} [target.'cfg(unix)'.dependencies] freedesktop-icons = "0.2.5" +freedesktop-desktop-entry = { version = "0.5.0", optional = true } +shlex = { version = "1.3.0", optional = true } [dependencies.cosmic-theme] path = "cosmic-theme" @@ -107,7 +111,6 @@ path = "./iced/futures" [dependencies.iced_accessibility] path = "./iced/accessibility" - optional = true [dependencies.iced_tiny_skia] diff --git a/src/desktop.rs b/src/desktop.rs new file mode 100644 index 00000000..9ec539f1 --- /dev/null +++ b/src/desktop.rs @@ -0,0 +1,213 @@ +pub use freedesktop_desktop_entry::DesktopEntry; +use std::{ + borrow::Cow, + ffi::OsStr, + path::{Path, PathBuf}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IconSource { + Name(String), + Path(PathBuf), +} + +impl IconSource { + pub fn from_unknown(icon: &str) -> Self { + let icon_path = Path::new(icon); + if icon_path.is_absolute() && icon_path.exists() { + Self::Path(icon_path.into()) + } else { + Self::Name(icon.into()) + } + } + + pub fn as_cosmic_icon(&self) -> crate::widget::icon::Icon { + match self { + Self::Name(name) => crate::widget::icon::from_name(name.as_str()) + .size(128) + .fallback(Some(crate::widget::icon::IconFallback::Names(vec![ + "application-default".into(), + "application-x-executable".into(), + ]))) + .into(), + Self::Path(path) => crate::widget::icon(crate::widget::icon::from_path(path.clone())), + } + } +} + +impl Default for IconSource { + fn default() -> Self { + Self::Name("application-default".to_string()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DesktopAction { + pub name: String, + pub exec: String, +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct DesktopEntryData { + pub id: String, + pub name: String, + pub wm_class: Option, + pub exec: Option, + pub icon: IconSource, + pub path: Option, + pub categories: String, + pub desktop_actions: Vec, + pub prefers_dgpu: bool, +} + +pub fn load_applications<'a>( + locale: impl Into>, + include_no_display: bool, +) -> Vec { + load_applications_filtered(locale, |de| include_no_display || !de.no_display()) +} + +pub fn load_applications_for_app_ids<'a, 'b>( + locale: impl Into>, + app_ids: impl Iterator, + fill_missing_ones: bool, +) -> Vec { + let mut app_ids = app_ids.collect::>(); + let mut applications = load_applications_filtered(locale, |de| { + if let Some(i) = app_ids + .iter() + .position(|id| id == &de.appid || id.eq(&de.startup_wm_class().unwrap_or_default())) + { + app_ids.remove(i); + true + } else { + false + } + }); + if fill_missing_ones { + applications.extend(app_ids.into_iter().map(|app_id| DesktopEntryData { + id: app_id.to_string(), + name: app_id.to_string(), + icon: IconSource::default(), + ..Default::default() + })); + } + applications +} + +pub fn load_applications_filtered<'a, F: FnMut(&DesktopEntry) -> bool>( + locale: impl Into>, + mut filter: F, +) -> Vec { + let locale = locale.into(); + + freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths()) + .filter_map(|path| { + std::fs::read_to_string(&path).ok().and_then(|input| { + DesktopEntry::decode(&path, &input).ok().and_then(|de| { + if !filter(&de) { + return None; + } + + Some(DesktopEntryData::from_desktop_entry( + locale, + path.clone(), + de, + )) + }) + }) + }) + .collect() +} + +pub fn load_desktop_file<'a>( + locale: impl Into>, + path: impl AsRef, +) -> Option { + let path = path.as_ref(); + std::fs::read_to_string(path).ok().and_then(|input| { + DesktopEntry::decode(path, &input) + .ok() + .map(|de| DesktopEntryData::from_desktop_entry(locale, PathBuf::from(path), de)) + }) +} + +impl DesktopEntryData { + fn from_desktop_entry<'a>( + locale: impl Into>, + path: impl Into>, + de: DesktopEntry, + ) -> DesktopEntryData { + let locale = locale.into(); + + let name = de + .name(locale) + .unwrap_or(Cow::Borrowed(de.appid)) + .to_string(); + + // check if absolute path exists and otherwise treat it as a name + let icon = de.icon().unwrap_or(de.appid); + let icon_path = Path::new(icon); + let icon = if icon_path.is_absolute() && icon_path.exists() { + IconSource::Path(icon_path.into()) + } else { + IconSource::Name(icon.into()) + }; + + DesktopEntryData { + id: de.appid.to_string(), + wm_class: de.startup_wm_class().map(ToString::to_string), + exec: de.exec().map(ToString::to_string), + name, + icon, + path: path.into(), + categories: de.categories().unwrap_or_default().to_string(), + desktop_actions: de + .actions() + .map(|actions| { + actions + .split(';') + .filter_map(|action| { + let name = de.action_entry_localized(action, "Name", locale); + let exec = de.action_entry(action, "Exec"); + if let (Some(name), Some(exec)) = (name, exec) { + Some(DesktopAction { + name: name.to_string(), + exec: exec.to_string(), + }) + } else { + None + } + }) + .collect::>() + }) + .unwrap_or_default(), + prefers_dgpu: de.prefers_non_default_gpu(), + } + } +} + +pub fn spawn_desktop_exec(exec: S, env_vars: I) +where + S: AsRef, + I: IntoIterator, + K: AsRef, + V: AsRef, +{ + let mut exec = shlex::Shlex::new(exec.as_ref()); + let mut cmd = match exec.next() { + Some(cmd) if !cmd.contains('=') => std::process::Command::new(cmd), + _ => return, + }; + + for arg in exec { + // TODO handle "%" args here if necessary? + if !arg.starts_with('%') { + cmd.arg(arg); + } + } + + cmd.envs(env_vars); + + crate::process::spawn(cmd) +} diff --git a/src/lib.rs b/src/lib.rs index 258b4e2f..2f22aac1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,8 @@ pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; +#[cfg(feature = "desktop")] +pub mod desktop; #[cfg(feature = "process")] pub mod process; From 693f776f7db8af8ecdbc7c90c9321a1a6db04809 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 31 Jan 2024 10:03:17 -0700 Subject: [PATCH 0092/1050] Allow debugging of all layout inside main view --- src/app/mod.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 9eee5274..5f814214 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -609,11 +609,11 @@ impl ApplicationExt for App { // Insert nav bar onto the left side of the window. if let Some(nav) = self.nav_bar() { - widgets.push(nav.debug(core.debug)); + widgets.push(nav); } if self.nav_model().is_none() || core.show_content() { - let main_content = self.view().debug(core.debug).map(Message::App); + let main_content = self.view().map(Message::App); widgets.push(if let Some(context) = self.context_drawer() { context_drawer( @@ -643,7 +643,7 @@ impl ApplicationExt for App { content_row.into() }; - crate::widget::column::with_capacity(2) + let view_element: Element<_> = crate::widget::column::with_capacity(2) .push_maybe(if core.window.show_headerbar { Some({ let mut header = crate::widget::header_bar() @@ -691,7 +691,8 @@ impl ApplicationExt for App { }) // The content element contains every element beneath the header. .push(content) - .into() + .into(); + view_element.debug(core.debug) } } From 029987850822d9bce15a5ab375a34b9cbb5b6c82 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 31 Jan 2024 10:03:39 -0700 Subject: [PATCH 0093/1050] Align headerbar items --- src/widget/header_bar.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 7de4de28..96be0f46 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -323,6 +323,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_x(iced::alignment::Horizontal::Right) .width(Length::Shrink), ) + .align_items(iced::Alignment::Center) .height(Length::Fixed(50.0)) .padding(8) .spacing(8) From ca1469a6b26eb7fbdc8e28d6135e86593e6a77fd Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 31 Jan 2024 10:51:31 -0700 Subject: [PATCH 0094/1050] Implement size_limits for winit --- src/app/mod.rs | 8 ++++++++ src/app/settings.rs | 3 --- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 5f814214..21274206 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -123,6 +123,14 @@ pub(crate) fn iced_settings( } iced.window.decorations = !settings.client_decorations; iced.window.size = settings.size; + let min_size = settings.size_limits.min(); + if min_size != iced::Size::ZERO { + iced.window.min_size = Some(min_size); + } + let max_size = settings.size_limits.max(); + if max_size != iced::Size::INFINITY { + iced.window.max_size = Some(max_size); + } iced.window.transparent = settings.transparent; } diff --git a/src/app/settings.rs b/src/app/settings.rs index 0606216d..48bdd729 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -4,7 +4,6 @@ //! Configure a new COSMIC application. use crate::{font, Theme}; -#[cfg(feature = "wayland")] use iced_core::layout::Limits; use iced_core::Font; @@ -51,7 +50,6 @@ pub struct Settings { pub(crate) size: iced::Size, /// Limitations of the window size - #[cfg(feature = "wayland")] pub(crate) size_limits: Limits, /// The theme to apply to the application. @@ -92,7 +90,6 @@ impl Default for Settings { .and_then(|scale| scale.parse::().ok()) .unwrap_or(1.0), size: iced::Size::new(1024.0, 768.0), - #[cfg(feature = "wayland")] size_limits: Limits::NONE.min_height(1.0).min_width(1.0), theme: crate::theme::system_preference(), transparent: true, From f4ad09864791cf110c3c8f3be078fd5703349da2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 30 Jan 2024 22:14:00 -0500 Subject: [PATCH 0095/1050] wip: update to use latest iced --- examples/cosmic/Cargo.toml | 2 +- examples/multi-window/Cargo.toml | 2 +- iced | 2 +- src/app/cosmic.rs | 2 +- src/applet/mod.rs | 39 +- src/ext.rs | 3 +- src/keyboard_nav.rs | 33 +- src/lib.rs | 4 +- src/theme/style/iced.rs | 418 ++++++++++++---------- src/theme/style/segmented_button.rs | 36 +- src/widget/aspect_ratio.rs | 34 +- src/widget/button/style.rs | 8 +- src/widget/button/widget.rs | 120 ++++--- src/widget/color_picker/mod.rs | 49 +-- src/widget/context_drawer/overlay.rs | 16 +- src/widget/context_drawer/widget.rs | 22 +- src/widget/cosmic_container.rs | 34 +- src/widget/dropdown/menu/appearance.rs | 4 +- src/widget/dropdown/menu/mod.rs | 63 ++-- src/widget/dropdown/multi/menu.rs | 67 ++-- src/widget/dropdown/multi/widget.rs | 27 +- src/widget/dropdown/widget.rs | 29 +- src/widget/flex_row/layout.rs | 10 +- src/widget/flex_row/widget.rs | 19 +- src/widget/frames.rs | 8 +- src/widget/grid/layout.rs | 14 +- src/widget/grid/widget.rs | 17 +- src/widget/header_bar.rs | 25 +- src/widget/icon/mod.rs | 4 +- src/widget/list/mod.rs | 13 +- src/widget/menu/flex.rs | 43 +-- src/widget/menu/menu_bar.rs | 45 ++- src/widget/menu/menu_inner.rs | 55 ++- src/widget/menu/menu_tree.rs | 11 +- src/widget/mod.rs | 19 +- src/widget/nav_bar.rs | 13 +- src/widget/popover.rs | 44 +-- src/widget/rectangle_tracker/mod.rs | 32 +- src/widget/scrollable.rs | 2 +- src/widget/segmented_button/horizontal.rs | 30 +- src/widget/segmented_button/style.rs | 6 +- src/widget/segmented_button/vertical.rs | 7 +- src/widget/segmented_button/widget.rs | 70 ++-- src/widget/spin_button/mod.rs | 10 +- src/widget/text.rs | 20 +- src/widget/text_input/input.rs | 257 +++++++------ src/widget/text_input/style.rs | 4 +- src/widget/toggler.rs | 2 +- src/widget/warning.rs | 16 +- 49 files changed, 956 insertions(+), 854 deletions(-) diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 7332a695..4a741a28 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.14.0" -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config", "wgpu"] } once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 18e0fff2..7a8e3051 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu"] } diff --git a/iced b/iced index 6115280d..32a0efcd 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 6115280d5277c50b8539d4eff6ab61050c51b592 +Subproject commit 32a0efcd05e827ba3ad9913e50dd103510e8bca7 diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 471db012..211bfc2e 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -22,7 +22,6 @@ use iced::window; #[cfg(not(any(feature = "multi-window", feature = "wayland")))] use iced::Application as IcedApplication; use iced_futures::event::listen_raw; -use iced_futures::futures::executor::block_on; #[cfg(not(feature = "wayland"))] use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] @@ -86,6 +85,7 @@ where fn new((mut core, flags): Self::Flags) -> (Self, iced::Command) { #[cfg(feature = "dbus-config")] { + use iced_futures::futures::executor::block_on; core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); } diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 45d769fd..2e6c8e24 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -16,7 +16,7 @@ use crate::{ }; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; -use iced_core::Padding; +use iced_core::{Padding, Shadow}; use iced_style::container::Appearance; use iced_widget::runtime::command::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, @@ -142,7 +142,7 @@ impl Context { pub fn popup_container<'a, Message: 'static>( &self, content: impl Into>, - ) -> Container<'a, Message, Renderer> { + ) -> Container<'a, Message, crate::Theme, Renderer> { let (vertical_align, horizontal_align) = match self.anchor { PanelAnchor::Left => (Vertical::Center, Horizontal::Left), PanelAnchor::Right => (Vertical::Center, Horizontal::Right), @@ -150,20 +150,25 @@ impl Context { PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), }; - Container::::new(Container::::new(content).style( - theme::Container::custom(|theme| { - let cosmic = theme.cosmic(); - let corners = cosmic.corner_radii.clone(); - Appearance { - text_color: Some(cosmic.background.on.into()), - background: Some(Color::from(cosmic.background.base).into()), - border_radius: corners.radius_m.into(), - border_width: 1.0, - border_color: cosmic.background.divider.into(), - icon_color: Some(cosmic.background.on.into()), - } - }), - )) + Container::::new( + Container::::new(content).style(theme::Container::custom( + |theme| { + let cosmic = theme.cosmic(); + let corners = cosmic.corner_radii.clone(); + Appearance { + text_color: Some(cosmic.background.on.into()), + background: Some(Color::from(cosmic.background.base).into()), + border: iced::Border { + radius: corners.radius_m.into(), + width: 1.0, + color: cosmic.background.divider.into(), + }, + shadow: Shadow::default(), + icon_color: Some(cosmic.background.on.into()), + } + }, + )), + ) .width(Length::Shrink) .height(Length::Shrink) .align_x(horizontal_align) @@ -306,7 +311,7 @@ pub fn menu_button<'a, Message>( pub fn padded_control<'a, Message>( content: impl Into>, -) -> crate::widget::container::Container<'a, Message, crate::Renderer> { +) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> { crate::widget::container(content) .padding(menu_control_padding()) .width(Length::Fill) diff --git a/src/ext.rs b/src/ext.rs index f182371f..beb18c56 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -20,7 +20,8 @@ impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> { } /// Additional methods for the [`Column`] and [`Row`] widgets. -pub trait CollectionWidget<'a, Message: 'a>: Widget +pub trait CollectionWidget<'a, Message: 'a>: + Widget where Self: Sized, { diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index 32bea5b2..eed4f84c 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -3,12 +3,9 @@ //! Subscribe to common application keyboard shortcuts. -use iced::{ - event, - keyboard::{self, KeyCode}, - mouse, Command, Event, Subscription, -}; +use iced::{event, keyboard, mouse, Command, Event, Subscription}; use iced_core::{ + keyboard::key::Named, widget::{operation, Id, Operation}, Rectangle, }; @@ -32,10 +29,11 @@ pub fn subscription() -> Subscription { match event { Event::Keyboard(keyboard::Event::KeyPressed { - key_code, + key: keyboard::Key::Named(key), modifiers, - }) => match key_code { - KeyCode::Tab => { + .. + }) => match key { + Named::Tab => { return Some(if modifiers.shift() { Message::FocusPrevious } else { @@ -43,24 +41,23 @@ pub fn subscription() -> Subscription { }); } - KeyCode::Escape => { + Named::Escape => { return Some(Message::Escape); } - KeyCode::F11 => { + Named::F11 => { return Some(Message::Fullscreen); } - KeyCode::F => { - return if modifiers.control() { - Some(Message::Search) - } else { - None - }; - } - _ => (), }, + Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Character(c), + modifiers, + .. + }) if c == "f" && modifiers.control() => { + return Some(Message::Search); + } Event::Mouse(mouse::Event::ButtonPressed { .. }) => { return Some(Message::Unfocus); diff --git a/src/lib.rs b/src/lib.rs index 2f22aac1..abb49e30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,5 +69,5 @@ pub use theme::{style, Theme}; pub mod widget; type Paragraph = ::Paragraph; -pub type Renderer = iced::Renderer; -pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; +pub type Renderer = iced::Renderer; +pub type Element<'a, Message> = iced::Element<'a, Message, crate::Theme, Renderer>; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 7a307588..18eda420 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -5,9 +5,7 @@ use crate::theme::{CosmicComponent, Theme, TRANSPARENT_COMPONENT}; use cosmic_theme::composite::over; -use iced_core::gradient::Linear; -use iced_core::BorderRadius; -use iced_core::Radians; +use iced_core::Border; use iced_core::{Background, Color}; use iced_style::application; use iced_style::button as iced_button; @@ -25,7 +23,7 @@ use iced_style::slider::Rail; use iced_style::svg; use iced_style::text_input; use iced_style::toggler; -use std::f32::consts::PI; + use std::rc::Rc; #[derive(Default)] @@ -203,14 +201,17 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: cosmic.accent.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.accent.base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.accent.base + } else { + cosmic.button.border + } + .into(), + }, + text_color: None, }, Checkbox::Secondary => checkbox::Appearance { @@ -220,9 +221,11 @@ impl checkbox::StyleSheet for Theme { cosmic.background.base.into() }), icon_color: cosmic.background.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: cosmic.button.border.into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: cosmic.button.border.into(), + }, text_color: None, }, Checkbox::Success => checkbox::Appearance { @@ -232,14 +235,16 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: cosmic.success.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.success.base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.success.base + } else { + cosmic.button.border + } + .into(), + }, text_color: None, }, Checkbox::Danger => checkbox::Appearance { @@ -249,14 +254,16 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: cosmic.destructive.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.destructive.base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.destructive.base + } else { + cosmic.button.border + } + .into(), + }, text_color: None, }, } @@ -274,14 +281,16 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: cosmic.accent.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.accent.base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.accent.base + } else { + cosmic.button.border + } + .into(), + }, text_color: None, }, Checkbox::Secondary => checkbox::Appearance { @@ -291,14 +300,16 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: self.current_container().on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - self.current_container().base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + self.current_container().base + } else { + cosmic.button.border + } + .into(), + }, text_color: None, }, Checkbox::Success => checkbox::Appearance { @@ -308,14 +319,16 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: cosmic.success.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.success.base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.success.base + } else { + cosmic.button.border + } + .into(), + }, text_color: None, }, Checkbox::Danger => checkbox::Appearance { @@ -325,14 +338,16 @@ impl checkbox::StyleSheet for Theme { cosmic.button.base.into() }), icon_color: cosmic.destructive.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.destructive.base - } else { - cosmic.button.border - } - .into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.destructive.base + } else { + cosmic.button.border + } + .into(), + }, text_color: None, }, } @@ -371,61 +386,50 @@ impl container::StyleSheet for Theme { match style { Container::Transparent => container::Appearance::default(), Container::Custom(f) => f(self), - Container::Background => { - let palette = self.cosmic(); - - container::Appearance { - icon_color: Some(Color::from(palette.background.on)), - text_color: Some(Color::from(palette.background.on)), - background: Some(iced::Background::Color(palette.background.base.into())), - border_radius: cosmic.corner_radii.radius_xs.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } + Container::Background => container::Appearance { + icon_color: Some(Color::from(cosmic.background.on)), + text_color: Some(Color::from(cosmic.background.on)), + background: Some(iced::Background::Color(cosmic.background.base.into())), + border: Border { + radius: cosmic.corner_radii.radius_xs.into(), + ..Default::default() + }, + shadow: Default::default(), + }, Container::HeaderBar => { - let palette = self.cosmic(); - let header_top = palette.background.base; + let header_top = cosmic.background.base; container::Appearance { - icon_color: Some(Color::from(palette.accent.base)), - text_color: Some(Color::from(palette.background.on)), + icon_color: Some(Color::from(cosmic.accent.base)), + text_color: Some(Color::from(cosmic.background.on)), background: Some(iced::Background::Color(header_top.into())), - border_radius: BorderRadius::from([ - palette.corner_radii.radius_xs[0], - palette.corner_radii.radius_xs[3], - 0.0, - 0.0, - ]), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_xs.into(), + ..Default::default() + }, + shadow: Default::default(), } } - Container::Primary => { - let palette = self.cosmic(); - - container::Appearance { - icon_color: Some(Color::from(palette.primary.on)), - text_color: Some(Color::from(palette.primary.on)), - background: Some(iced::Background::Color(palette.primary.base.into())), - border_radius: cosmic.corner_radii.radius_xs.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - Container::Secondary => { - let palette = self.cosmic(); - - container::Appearance { - icon_color: Some(Color::from(palette.secondary.on)), - text_color: Some(Color::from(palette.secondary.on)), - background: Some(iced::Background::Color(palette.secondary.base.into())), - border_radius: cosmic.corner_radii.radius_xs.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - + Container::Primary => container::Appearance { + icon_color: Some(Color::from(cosmic.primary.on)), + text_color: Some(Color::from(cosmic.primary.on)), + background: Some(iced::Background::Color(cosmic.primary.base.into())), + border: Border { + radius: cosmic.corner_radii.radius_xs.into(), + ..Default::default() + }, + shadow: Default::default(), + }, + Container::Secondary => container::Appearance { + icon_color: Some(Color::from(cosmic.secondary.on)), + text_color: Some(Color::from(cosmic.secondary.on)), + background: Some(iced::Background::Color(cosmic.secondary.base.into())), + border: Border { + radius: cosmic.corner_radii.radius_xs.into(), + ..Default::default() + }, + shadow: Default::default(), + }, Container::Dropdown => { let theme = self.cosmic(); @@ -433,58 +437,63 @@ impl container::StyleSheet for Theme { icon_color: None, text_color: None, background: Some(iced::Background::Color(theme.primary.base.into())), - border_radius: theme.corner_radii.radius_xs.into(), - border_width: 1.0, - border_color: theme.bg_divider().into(), - } - } - - Container::Tooltip => { - let theme = self.cosmic(); - - container::Appearance { - icon_color: None, - text_color: None, - background: Some(iced::Background::Color(theme.palette.neutral_2.into())), - border_radius: theme.corner_radii.radius_l.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_xs.into(), + ..Default::default() + }, + shadow: Default::default(), } } + Container::Tooltip => container::Appearance { + icon_color: None, + text_color: None, + background: Some(iced::Background::Color(cosmic.palette.neutral_2.into())), + border: Border { + radius: cosmic.corner_radii.radius_l.into(), + ..Default::default() + }, + shadow: Default::default(), + }, Container::Card => { - let palette = self.cosmic(); + let cosmic = self.cosmic(); match self.layer { cosmic_theme::Layer::Background => container::Appearance { - icon_color: Some(Color::from(palette.background.component.on)), - text_color: Some(Color::from(palette.background.component.on)), + icon_color: Some(Color::from(cosmic.background.component.on)), + text_color: Some(Color::from(cosmic.background.component.on)), background: Some(iced::Background::Color( - palette.background.component.base.into(), + cosmic.background.component.base.into(), )), - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + shadow: Default::default(), }, cosmic_theme::Layer::Primary => container::Appearance { - icon_color: Some(Color::from(palette.primary.component.on)), - text_color: Some(Color::from(palette.primary.component.on)), + icon_color: Some(Color::from(cosmic.primary.component.on)), + text_color: Some(Color::from(cosmic.primary.component.on)), background: Some(iced::Background::Color( - palette.primary.component.base.into(), + cosmic.primary.component.base.into(), )), - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + shadow: Default::default(), }, cosmic_theme::Layer::Secondary => container::Appearance { - icon_color: Some(Color::from(palette.secondary.component.on)), - text_color: Some(Color::from(palette.secondary.component.on)), + icon_color: Some(Color::from(cosmic.secondary.component.on)), + text_color: Some(Color::from(cosmic.secondary.component.on)), background: Some(iced::Background::Color( - palette.secondary.component.base.into(), + cosmic.secondary.component.base.into(), )), - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + shadow: Default::default(), }, } } @@ -581,9 +590,10 @@ impl menu::StyleSheet for Theme { menu::Appearance { text_color: cosmic.on_bg_color().into(), background: Background::Color(cosmic.background.base.into()), - border_width: 0.0, - border_radius: cosmic.corner_radii.radius_m.into(), - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_m.into(), + ..Default::default() + }, selected_text_color: cosmic.accent.base.into(), selected_background: Background::Color(cosmic.background.component.hover.into()), } @@ -603,9 +613,10 @@ impl pick_list::StyleSheet for Theme { text_color: cosmic.on_bg_color().into(), background: Color::TRANSPARENT.into(), placeholder_color: cosmic.on_bg_color().into(), - border_radius: cosmic.corner_radii.radius_m.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_m.into(), + ..Default::default() + }, // icon_size: 0.7, // TODO: how to replace handle_color: cosmic.on_bg_color().into(), } @@ -740,9 +751,11 @@ impl pane_grid::StyleSheet for Theme { let theme = self.cosmic(); pane_grid::Appearance { background: Background::Color(theme.bg_color().into()), - border_width: 2.0, - border_color: theme.bg_divider().into(), - border_radius: theme.corner_radii.radius_0.into(), + border: Border { + radius: theme.corner_radii.radius_0.into(), + width: 2.0, + color: theme.bg_divider().into(), + }, } } } @@ -857,14 +870,16 @@ impl scrollable::StyleSheet for Theme { neutral_5.alpha = 0.7; let mut a = scrollable::Scrollbar { background: None, - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, scroller: scrollable::Scroller { color: neutral_5.into(), - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, }, }; @@ -889,14 +904,16 @@ impl scrollable::StyleSheet for Theme { } let mut a = scrollable::Scrollbar { background: None, - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, scroller: scrollable::Scroller { color: neutral_5.into(), - border_radius: cosmic.corner_radii.radius_s.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, }, }; if matches!(style, Scrollable::Permanent) { @@ -934,6 +951,10 @@ impl svg::StyleSheet for Theme { Svg::Custom(appearance) => appearance(self), } } + + fn hovered(&self, style: &Self::Style) -> svg::Appearance { + self.appearance(style) + } } /* @@ -990,16 +1011,19 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: palette.corner_radii.radius_s.into(), - border_width: 1.0, - border_color: self.current_container().component.divider.into(), + border: Border { + radius: palette.corner_radii.radius_s.into(), + width: 1.0, + color: self.current_container().component.divider.into(), + }, icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), - border_radius: palette.corner_radii.radius_m.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: palette.corner_radii.radius_m.into(), + ..Default::default() + }, icon_color: self.current_container().on.into(), }, } @@ -1013,16 +1037,19 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: palette.corner_radii.radius_s.into(), - border_width: 1.0, - border_color: palette.accent.base.into(), + border: Border { + radius: palette.corner_radii.radius_s.into(), + width: 1.0, + color: self.current_container().on.into(), + }, icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), - border_radius: palette.corner_radii.radius_m.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: palette.corner_radii.radius_m.into(), + ..Default::default() + }, icon_color: self.current_container().on.into(), }, } @@ -1036,16 +1063,19 @@ impl text_input::StyleSheet for Theme { match style { TextInput::Default => text_input::Appearance { background: Color::from(bg).into(), - border_radius: palette.corner_radii.radius_s.into(), - border_width: 1.0, - border_color: palette.accent.base.into(), + border: Border { + radius: palette.corner_radii.radius_s.into(), + width: 1.0, + color: palette.accent.base.into(), + }, icon_color: self.current_container().on.into(), }, TextInput::Search => text_input::Appearance { background: Color::from(bg).into(), - border_radius: palette.corner_radii.radius_m.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: palette.corner_radii.radius_m.into(), + ..Default::default() + }, icon_color: self.current_container().on.into(), }, } @@ -1121,9 +1151,11 @@ impl iced_style::text_editor::StyleSheet for Theme { let cosmic = self.cosmic(); iced_style::text_editor::Appearance { background: iced::Color::from(cosmic.bg_color()).into(), - border_radius: cosmic.corner_radii.radius_0.into(), - border_width: f32::from(cosmic.space_xxxs()), - border_color: iced::Color::from(cosmic.bg_divider()), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.bg_divider()), + }, } } @@ -1135,9 +1167,11 @@ impl iced_style::text_editor::StyleSheet for Theme { let cosmic = self.cosmic(); iced_style::text_editor::Appearance { background: iced::Color::from(cosmic.bg_color()).into(), - border_radius: cosmic.corner_radii.radius_0.into(), - border_width: f32::from(cosmic.space_xxxs()), - border_color: iced::Color::from(cosmic.accent.base), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.accent.base), + }, } } diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 8d6b2c12..c3c87a64 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -5,7 +5,7 @@ use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; -use iced_core::{Background, BorderRadius}; +use iced_core::{border::Radius, Background}; #[derive(Default)] pub enum SegmentedButton { @@ -66,9 +66,7 @@ impl StyleSheet for Theme { inactive: ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([ - rad_m[0], rad_0[1], rad_0[2], rad_m[3], - ]), + border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), ..Default::default() }, middle: ItemAppearance { @@ -76,9 +74,7 @@ impl StyleSheet for Theme { ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([ - rad_0[0], rad_m[1], rad_m[2], rad_0[3], - ]), + border_radius: Radius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), ..Default::default() }, text_color: cosmic.on_bg_color().into(), @@ -123,9 +119,7 @@ impl StyleSheet for Theme { inactive: ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([ - rad_m[0], rad_m[1], rad_0[0], rad_0[0], - ]), + border_radius: Radius::from([rad_m[0], rad_m[1], rad_0[0], rad_0[0]]), ..Default::default() }, middle: ItemAppearance { @@ -133,9 +127,7 @@ impl StyleSheet for Theme { ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([ - rad_0[0], rad_0[1], rad_m[2], rad_m[3], - ]), + border_radius: Radius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), ..Default::default() }, text_color: cosmic.on_bg_color().into(), @@ -153,7 +145,7 @@ impl StyleSheet for Theme { mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use iced_core::{Background, BorderRadius}; + use iced_core::{border::Radius, Background}; pub fn selection_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; @@ -163,7 +155,7 @@ mod horizontal { ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), + border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), ..Default::default() }, middle: ItemAppearance { @@ -171,7 +163,7 @@ mod horizontal { ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), + border_radius: Radius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), ..Default::default() }, text_color: cosmic.accent.base.into(), @@ -186,17 +178,17 @@ mod horizontal { ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), + border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, middle: ItemAppearance { - border_radius: BorderRadius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), + border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), + border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, @@ -229,7 +221,7 @@ pub fn hover(cosmic: &cosmic_theme::Theme, default: &ItemStatusAppearance) -> It mod vertical { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use iced_core::{Background, BorderRadius}; + use iced_core::{border::Radius, Background}; pub fn selection_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { let mut neutral_5 = cosmic.palette.neutral_5; @@ -239,7 +231,7 @@ mod vertical { ItemStatusAppearance { background: Some(Background::Color(neutral_5.into())), first: ItemAppearance { - border_radius: BorderRadius::from([rad_m[0], rad_m[1], rad_0[2], rad_0[3]]), + border_radius: Radius::from([rad_m[0], rad_m[1], rad_0[2], rad_0[3]]), ..Default::default() }, middle: ItemAppearance { @@ -247,7 +239,7 @@ mod vertical { ..Default::default() }, last: ItemAppearance { - border_radius: BorderRadius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), + border_radius: Radius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), ..Default::default() }, text_color: cosmic.accent.base.into(), diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 471cfaaf..32ef16e5 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -16,7 +16,7 @@ pub fn aspect_ratio_container<'a, Message: 'static, T>( ratio: f32, ) -> AspectRatio<'a, Message, crate::Renderer> where - T: Into>, + T: Into>, { AspectRatio::new(content, ratio) } @@ -28,16 +28,14 @@ where pub struct AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, { ratio: f32, - container: Container<'a, Message, Renderer>, + container: Container<'a, Message, crate::Theme, Renderer>, } impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, { fn constrain_limits(&self, size: Size) -> Size { let Size { @@ -56,12 +54,11 @@ where impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T, ratio: f32) -> Self where - T: Into>, + T: Into>, { AspectRatio { ratio, @@ -134,16 +131,16 @@ where /// Sets the style of the [`Container`]. #[must_use] - pub fn style(mut self, style: impl Into<::Style>) -> Self { + pub fn style(mut self, style: impl Into<::Style>) -> Self { self.container = self.container.style(style); self } } -impl<'a, Message, Renderer> Widget for AspectRatio<'a, Message, Renderer> +impl<'a, Message, Renderer> Widget + for AspectRatio<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, { fn children(&self) -> Vec { self.container.children() @@ -153,12 +150,8 @@ where self.container.diff(tree); } - fn width(&self) -> Length { - Widget::width(&self.container) - } - - fn height(&self) -> Length { - Widget::height(&self.container) + fn size(&self) -> Size { + self.container.size() } fn layout( @@ -226,7 +219,7 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &Renderer::Theme, + theme: &crate::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor_position: mouse::Cursor, @@ -248,19 +241,20 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { + ) -> Option> { self.container.overlay(tree, layout, renderer) } } impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> + for Element<'a, Message, crate::Theme, Renderer> where Message: 'a, Renderer: 'a + iced_core::Renderer, - Renderer::Theme: StyleSheet, { - fn from(column: AspectRatio<'a, Message, Renderer>) -> Element<'a, Message, Renderer> { + fn from( + column: AspectRatio<'a, Message, Renderer>, + ) -> Element<'a, Message, crate::Theme, Renderer> { Element::new(column) } } diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs index 88a22539..9cda41e9 100644 --- a/src/widget/button/style.rs +++ b/src/widget/button/style.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 //! Change the apperance of a button. -use iced_core::{Background, BorderRadius, Color, Vector}; +use iced_core::{border::Radius, Background, Color, Vector}; use crate::theme::THEME; @@ -17,7 +17,7 @@ pub struct Appearance { pub background: Option, /// The border radius of the button. - pub border_radius: BorderRadius, + pub border_radius: Radius, /// The border width of the button. pub border_width: f32, @@ -39,13 +39,13 @@ pub struct Appearance { } impl Appearance { - // TODO: `BorderRadius` is not `const fn` compatible. + // TODO: `Radius` is not `const fn` compatible. pub fn new() -> Self { let rad_0 = THEME.with(|t| t.borrow().cosmic().corner_radii.radius_0); Self { shadow_offset: Vector::new(0.0, 0.0), background: None, - border_radius: BorderRadius::from(rad_0), + border_radius: Radius::from(rad_0), border_width: 0.0, border_color: Color::TRANSPARENT, outline_width: 0.0, diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 7e5e301b..ffc94c3f 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -10,13 +10,13 @@ use iced_runtime::core::widget::Id; use iced_runtime::{keyboard, Command}; use iced_core::event::{self, Event}; -use iced_core::mouse; -use iced_core::overlay; use iced_core::renderer::{self, Quad}; use iced_core::touch; use iced_core::widget::tree::{self, Tree}; use iced_core::widget::Operation; use iced_core::{layout, svg}; +use iced_core::{mouse, Border}; +use iced_core::{overlay, Shadow}; use iced_core::{ Background, Clipboard, Color, Element, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, @@ -43,7 +43,6 @@ enum Variant { pub struct Button<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, { id: Id, #[cfg(feature = "a11y")] @@ -52,23 +51,22 @@ where description: Option>, #[cfg(feature = "a11y")] label: Option>, - content: Element<'a, Message, Renderer>, + content: Element<'a, Message, crate::Theme, Renderer>, on_press: Option, width: Length, height: Length, padding: Padding, selected: bool, - style: ::Style, + style: ::Style, variant: Variant, } impl<'a, Message, Renderer> Button<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Renderer::Theme: StyleSheet, { /// Creates a new [`Button`] with the given content. - pub fn new(content: impl Into>) -> Self { + pub fn new(content: impl Into>) -> Self { Self { id: Id::unique(), #[cfg(feature = "a11y")] @@ -83,13 +81,13 @@ where height: Length::Shrink, padding: Padding::new(5.0), selected: false, - style: ::Style::default(), + style: ::Style::default(), variant: Variant::Normal, } } pub fn new_image( - content: impl Into>, + content: impl Into>, on_remove: Option, ) -> Self { Self { @@ -106,7 +104,7 @@ where height: Length::Shrink, padding: Padding::new(5.0), selected: false, - style: ::Style::default(), + style: ::Style::default(), variant: Variant::Image { on_remove, close_icon: crate::widget::icon::from_name("window-close-symbolic") @@ -180,7 +178,7 @@ where } /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: ::Style) -> Self { + pub fn style(mut self, style: ::Style) -> Self { self.style = style; self } @@ -216,11 +214,11 @@ where } } -impl<'a, Message, Renderer> Widget for Button<'a, Message, Renderer> +impl<'a, Message, Renderer> Widget + for Button<'a, Message, Renderer> where Message: 'a + Clone, Renderer: 'a + iced_core::Renderer + svg::Renderer, - Renderer::Theme: StyleSheet, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -238,12 +236,8 @@ where tree.diff_children(std::slice::from_mut(&mut self.content)); } - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> iced_core::Size { + iced_core::Size::new(self.width, self.height) } fn layout( @@ -346,7 +340,7 @@ where &self, tree: &Tree, renderer: &mut Renderer, - theme: &Renderer::Theme, + theme: &crate::Theme, renderer_style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, @@ -400,15 +394,17 @@ where x: bounds.x + styling.border_width, y: bounds.y + (bounds.height - 20.0 - styling.border_width), }, - border_radius: [ - c_rad.radius_0[0], - c_rad.radius_s[1], - c_rad.radius_0[2], - c_rad.radius_s[3], - ] - .into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + border: Border { + radius: [ + c_rad.radius_0[0], + c_rad.radius_s[1], + c_rad.radius_0[2], + c_rad.radius_s[3], + ] + .into(), + ..Default::default() + }, + shadow: Default::default(), }, selection_background, ); @@ -433,9 +429,11 @@ where renderer.fill_quad( renderer::Quad { bounds, - border_radius: c_rad.radius_m.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, + shadow: Default::default(), + border: Border { + radius: c_rad.radius_m.into(), + ..Default::default() + }, }, selection_background, ); @@ -473,7 +471,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - ) -> Option> { + ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), @@ -556,11 +554,11 @@ where } } -impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From> + for Element<'a, Message, crate::Theme, Renderer> where Message: Clone + 'a, Renderer: iced_core::Renderer + svg::Renderer + 'a, - Renderer::Theme: StyleSheet, { fn from(button: Button<'a, Message, Renderer>) -> Self { Self::new(button) @@ -660,10 +658,10 @@ pub fn update<'a, Message: Clone>( } return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press.clone() { let state = state(); - if state.is_focused && key_code == keyboard::KeyCode::Enter { + if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { state.is_pressed = true; shell.publish(on_press); return event::Status::Captured; @@ -688,13 +686,12 @@ pub fn draw<'a, Renderer: iced_core::Renderer>( cursor: mouse::Cursor, is_enabled: bool, is_selected: bool, - style_sheet: &dyn StyleSheet