diff --git a/Cargo.toml b/Cargo.toml index 8696f88..5e7cb83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,22 @@ rust-version = "1.80" name = "cosmic" [features] -default = ["clipboard"] # Accessibility support 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"] +# XXX autosize should not be used on winit windows unless dialogs +autosize = [] +applet = [ + "a11y", + "autosize", + "wayland", + "tokio", + "cosmic-panel-config", + "ron", + "multi-window", +] applet-token = [] -clipboard = ["iced_sctk?/clipboard"] # Use the cosmic-settings-daemon for config handling dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"] # Debug features @@ -61,7 +68,7 @@ wayland = [ "ashpd?/wayland", "iced_runtime/wayland", "iced/wayland", - "iced_sctk", + "iced_winit/wayland", "cctk", ] # multi-window support @@ -120,7 +127,7 @@ path = "cosmic-theme" [dependencies.iced] path = "./iced" default-features = false -features = ["advanced", "image", "lazy", "svg", "web-colors"] +features = ["advanced", "image", "lazy", "svg", "web-colors", "tiny-skia"] [dependencies.iced_runtime] path = "./iced/runtime" @@ -146,13 +153,6 @@ optional = true [dependencies.iced_tiny_skia] path = "./iced/tiny_skia" -[dependencies.iced_style] -path = "./iced/style" - -[dependencies.iced_sctk] -path = "./iced/sctk" -optional = true - [dependencies.iced_winit] path = "./iced/winit" optional = true @@ -189,3 +189,9 @@ dirs = "5.0.1" [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./" } + +# FIXME update winit deps where necessary to use this +# [patch.crates-io] +[patch."https://github.com/pop-os/winit.git"] +# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" } +# winit = { path = "../../winit" } diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index a36d49d..dd0612c 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -3,7 +3,10 @@ use std::ops::Deref; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; -use iced_futures::futures::{self, future::pending, StreamExt}; +use iced_futures::{ + futures::{self, future::pending, Stream, StreamExt}, + stream, Subscription, +}; pub async fn settings_daemon_proxy() -> zbus::Result> { let conn = zbus::Connection::session().await?; @@ -58,13 +61,20 @@ pub fn watcher_subscription iced_futures::Subscription> { + let id = std::any::TypeId::of::(); + Subscription::run_with_id(id, watcher_stream(settings_daemon, config_id, is_state)) +} + +fn watcher_stream( + settings_daemon: CosmicSettingsDaemonProxy<'static>, + config_id: &'static str, + is_state: bool, +) -> impl Stream> { enum Change { Changes(Changed), OwnerChanged(bool), } - - let id = std::any::TypeId::of::(); - iced_futures::subscription::channel((is_state, config_id, id), 5, move |mut tx| async move { + stream::channel(5, move |mut tx| async move { let version = T::VERSION; let Ok(cosmic_config) = (if is_state { diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 48a18a5..f915da7 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -384,6 +384,7 @@ where ) -> (Vec, Vec<&'static str>); } +#[derive(Debug)] pub struct Update { pub errors: Vec, pub keys: Vec<&'static str>, diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index ef88866..7468af0 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -1,5 +1,5 @@ -use iced_futures::futures::SinkExt; -use iced_futures::{futures::channel::mpsc, subscription}; +use iced_futures::futures::{SinkExt, Stream}; +use iced_futures::{futures::channel::mpsc, stream}; use notify::RecommendedWatcher; use std::{borrow::Cow, hash::Hash}; @@ -24,17 +24,7 @@ pub fn config_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - 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; - } - } - }) + iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false)) } pub fn config_state_subscription< @@ -45,26 +35,30 @@ pub fn config_state_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - subscription::channel(id, 100, move |mut output| { + iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true)) +} + +fn watcher_stream( + config_id: Cow<'static, str>, + config_version: u64, + is_state: bool, +) -> impl Stream> { + stream::channel(100, move |mut output| { let config_id = config_id.clone(); async move { let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, true); + let mut state = ConfigState::Init(config_id, config_version, is_state); loop { - state = start_listening(state, &mut output, id).await; + state = start_listening::(state, &mut output).await; } } }) } -async fn start_listening< - I: Copy, - T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, ->( +async fn start_listening( state: ConfigState, output: &mut mpsc::Sender>, - id: I, ) -> ConfigState { use iced_futures::futures::{future::pending, StreamExt}; diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 965e30a..ba4ce66 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -9,8 +9,20 @@ edition = "2021" once_cell = "1" rust-embed = "8.0.0" tracing = "0.1" +env_logger = "0.10.0" +log = "0.4.17" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" default-features = false -features = ["applet", "tokio", "wayland"] +features = [ + "applet", + "applet-token", + "multi-window", + "tokio", + "wayland", + "winit", + "desktop", + "dbus-config", + "image", +] diff --git a/examples/applet/src/main.rs b/examples/applet/src/main.rs index 28e893a..4ff0c0c 100644 --- a/examples/applet/src/main.rs +++ b/examples/applet/src/main.rs @@ -3,5 +3,10 @@ use crate::window::Window; mod window; fn main() -> cosmic::iced::Result { - cosmic::applet::run::(true, ()) + let env = env_logger::Env::default() + .filter_or("MY_LOG_LEVEL", "warn") + .write_style_or("MY_LOG_STYLE", "always"); + + env_logger::init_from_env(env); + cosmic::applet::run::(()) } diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index c1706c6..5830657 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,9 +1,10 @@ use cosmic::app::Core; -use cosmic::iced::wayland::popup::{destroy_popup, get_popup}; +use cosmic::iced::application; +use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get_popup}; use cosmic::iced::window::Id; -use cosmic::iced::{Command, Limits}; +use cosmic::iced::{Length, Limits, Task}; use cosmic::iced_runtime::core::window; -use cosmic::iced_style::application; +use cosmic::theme::iced; use cosmic::widget::{list_column, settings, toggler}; use cosmic::{Element, Theme}; @@ -37,22 +38,19 @@ impl cosmic::Application for Window { &mut self.core } - fn init( - core: Core, - _flags: Self::Flags, - ) -> (Self, Command>) { + fn init(core: Core, _flags: Self::Flags) -> (Self, Task>) { let window = Window { core, ..Default::default() }; - (window, Command::none()) + (window, Task::none()) } fn on_close_requested(&self, id: window::Id) -> Option { Some(Message::PopupClosed(id)) } - fn update(&mut self, message: Self::Message) -> Command> { + fn update(&mut self, message: Self::Message) -> Task> { match message { Message::TogglePopup => { return if let Some(p) = self.popup.take() { @@ -60,17 +58,23 @@ impl cosmic::Application for Window { } else { let new_id = Id::unique(); self.popup.replace(new_id); - let mut popup_settings = - self.core - .applet - .get_popup_settings(Id::MAIN, new_id, None, None, None); + let mut popup_settings = self.core.applet.get_popup_settings( + self.core.main_window_id().unwrap(), + new_id, + None, + None, + None, + ); popup_settings.positioner.size_limits = Limits::NONE .max_width(372.0) .min_width(300.0) .min_height(200.0) - .max_height(1080.0); + .max_height(1080.0) + .height(500) + .width(500); + popup_settings.positioner.size = Some((500, 500)); get_popup(popup_settings) - } + }; } Message::PopupClosed(id) => { if self.popup.as_ref() == Some(&id) { @@ -79,7 +83,7 @@ impl cosmic::Application for Window { } Message::ToggleExampleRow(toggled) => self.example_row = toggled, } - Command::none() + Task::none() } fn view(&self) -> Element { @@ -93,15 +97,16 @@ impl cosmic::Application for Window { fn view_window(&self, _id: Id) -> Element { let content_list = list_column().padding(5).spacing(0).add(settings::item( "Example row", - toggler(None, self.example_row, |value| { + cosmic::widget::container(toggler(self.example_row, |value| { Message::ToggleExampleRow(value) - }), + })) + .height(Length::Fixed(50.)), )); self.core.applet.popup_container(content_list).into() } - fn style(&self) -> Option<::Style> { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 7f188b2..695f989 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,4 +11,15 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" default-features = false -features = ["debug", "winit", "tokio", "xdg-portal", "dbus-config", "a11y"] +features = [ + "debug", + "winit", + "tokio", + "xdg-portal", + "dbus-config", + "a11y", + "wayland", + "wgpu", + "single-instance", + "multi-window", +] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 0eba0a8..24568f0 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -3,7 +3,8 @@ //! Application API example -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::widget::column; use cosmic::iced_core::Size; use cosmic::widget::nav_bar; use cosmic::{executor, iced, ApplicationExt, Element}; @@ -50,12 +51,17 @@ fn main() -> Result<(), Box> { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] -pub enum Message {} +pub enum Message { + Input1(String), + Input2(String), +} /// The [`App`] stores application-specific state. pub struct App { core: Core, nav_model: nav_bar::Model, + input_1: String, + input_2: String, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -81,7 +87,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, input: Self::Flags) -> (Self, Command) { + fn init(core: Core, input: Self::Flags) -> (Self, Task) { let mut nav_model = nav_bar::Model::default(); for (title, content) in input { @@ -90,7 +96,12 @@ impl cosmic::Application for App { nav_model.activate_position(0); - let mut app = App { core, nav_model }; + let mut app = App { + core, + nav_model, + input_1: String::new(), + input_2: String::new(), + }; let command = app.update_title(); @@ -103,14 +114,22 @@ impl cosmic::Application for App { } /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { self.nav_model.activate(id); self.update_title() } /// Handle application events here. - fn update(&mut self, _message: Self::Message) -> Command { - Command::none() + fn update(&mut self, message: Self::Message) -> Task { + match message { + Message::Input1(v) => { + self.input_1 = v; + } + Message::Input2(v) => { + self.input_2 = v; + } + } + Task::none() } /// Creates a view after each update. @@ -122,11 +141,20 @@ impl cosmic::Application for App { let text = cosmic::widget::text(page_content); - let centered = cosmic::widget::container(text) + let centered = cosmic::widget::container( + column![ + text, + cosmic::widget::text_input::text_input("", &self.input_1).on_input(Message::Input1), + cosmic::widget::text_input::text_input("", &self.input_2).on_input(Message::Input2), + ] .width(iced::Length::Fill) .height(iced::Length::Shrink) - .align_x(iced::alignment::Horizontal::Center) - .align_y(iced::alignment::Vertical::Center); + .align_x(iced::alignment::Horizontal::Center), + ) + .width(iced::Length::Fill) + .height(iced::Length::Shrink) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center); Element::from(centered) } @@ -142,10 +170,14 @@ where .unwrap_or("Unknown Page") } - fn update_title(&mut self) -> Command { + fn update_title(&mut self) -> Task { let header_title = self.active_page_title().to_owned(); let window_title = format!("{header_title} — COSMIC AppDemo"); self.set_header_title(header_title); - self.set_window_title(window_title) + if let Some(id) = self.core.main_window_id() { + self.set_window_title(window_title, id) + } else { + Task::none() + } } } diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 1b20f35..df36b73 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -4,7 +4,7 @@ //! Calendar widget example use chrono::{Local, NaiveDate}; -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Core, Settings, Task}; use cosmic::{executor, iced, ApplicationExt, Element}; /// Runs application with these settings @@ -50,7 +50,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { let now = Local::now(); let mut app = App { @@ -64,7 +64,7 @@ impl cosmic::Application for App { } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::DateSelected(date) => { self.date_selected = date; @@ -73,7 +73,7 @@ impl cosmic::Application for App { println!("Date selected: {:?}", self.date_selected); - Command::none() + Task::none() } /// Creates a view after each update. @@ -99,7 +99,7 @@ impl App where Self: cosmic::Application, { - fn update_title(&mut self) -> Command { + fn update_title(&mut self) -> Task { self.set_header_title(String::from("Calendar Demo")); self.set_window_title(String::from("Calendar Demo")) } diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index cfc9918..6607722 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -3,7 +3,7 @@ //! Application API example -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Task, Core, Settings}; use cosmic::iced_core::Size; use cosmic::widget::{menu, segmented_button}; use cosmic::{executor, iced, ApplicationExt, Element}; @@ -65,7 +65,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { let mut app = App { core, button_label: String::from("Right click me"), @@ -80,10 +80,10 @@ impl cosmic::Application for App { } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { self.button_label = format!("Clicked {message:?}"); - Command::none() + Task::none() } /// Creates a view after each update. diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index b478534..a554772 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -6,7 +6,7 @@ use cosmic::{ ThemeBuilder, }, font::load_fonts, - iced::{self, Application, Command, Length, Subscription}, + iced::{self, Application, Length, Subscription, Task}, iced::{ subscription, widget::{self, column, container, horizontal_space, row, text}, @@ -324,7 +324,7 @@ impl Application for Window { type Message = Message; type Theme = Theme; - fn new(_flags: ()) -> (Self, Command) { + fn new(_flags: ()) -> (Self, Task) { let mut window = Window::default() .nav_bar_toggled(true) .show_maximize(true) @@ -389,8 +389,8 @@ impl Application for Window { ]) } - fn update(&mut self, message: Message) -> iced::Command { - let mut ret = Command::none(); + fn update(&mut self, message: Message) -> iced::Task { + let mut ret = Task::none(); match message { Message::NavBar(key) => { if let Some(page) = self.nav_id_to_page.get(key).copied() { @@ -437,10 +437,10 @@ impl Application for Window { Message::ToggleNavBarCondensed => { self.nav_bar_toggled_condensed = !self.nav_bar_toggled_condensed } - 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::Drag => return drag(self.core.main_window_id().unwrap()), + Message::Close => return close(self.core.main_window_id().unwrap()), + Message::Minimize => return minimize(self.core.main_window_id().unwrap(), true), + Message::Maximize => return toggle_maximize(self.core.main_window_id().unwrap()), Message::InputChanged => {} diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index f009793..9ca84ef 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -482,7 +482,7 @@ impl State { )) .layer(cosmic::cosmic_theme::Layer::Secondary) .padding(16) - .style(cosmic::theme::Container::Background) + .class(cosmic::theme::Container::Background) .into(), cosmic::widget::text_input::secure_input( "Type to search apps or type “?” for more options...", diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index bfa51ba..40cb70b 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -3,7 +3,7 @@ //! Application API example -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Task, Core, Settings}; use cosmic::{executor, iced, ApplicationExt, Element}; /// Runs application with these settings @@ -51,7 +51,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { let mut app = App { core, selected: 0, @@ -67,7 +67,7 @@ impl cosmic::Application for App { } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::Clicked(id) => self.selected = id, Message::Remove(id) => { @@ -75,7 +75,7 @@ impl cosmic::Application for App { } } - Command::none() + Task::none() } /// Creates a view after each update. @@ -106,7 +106,7 @@ impl App where Self: cosmic::Application, { - fn update_title(&mut self) -> Command { + fn update_title(&mut self) -> Task { self.set_header_title(String::from("Image Button Demo")); self.set_window_title(String::from("Image Button Demo")) } diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index ab668cb..c7c686d 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::{env, process}; -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Task, Core, Settings}; use cosmic::iced::window; use cosmic::iced_core::alignment::{Horizontal, Vertical}; use cosmic::iced_core::keyboard::Key; @@ -97,7 +97,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { let app = App { core, config: Config { @@ -106,7 +106,7 @@ impl cosmic::Application for App { key_binds: key_binds(), }; - (app, Command::none()) + (app, Task::none()) } fn header_start(&self) -> Vec> { @@ -114,13 +114,13 @@ impl cosmic::Application for App { } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::WindowClose => { - return window::close(window::Id::MAIN); + return window::close(self.core.main_window_id().unwrap()); } Message::WindowNew => match env::current_exe() { - Ok(exe) => match process::Command::new(&exe).spawn() { + Ok(exe) => match process::Task::new(&exe).spawn() { Ok(_child) => {} Err(err) => { eprintln!("failed to execute {:?}: {}", exe, err); @@ -132,7 +132,7 @@ impl cosmic::Application for App { }, Message::ToggleHideContent => self.config.hide_content = !self.config.hide_content, } - Command::none() + Task::none() } /// Creates a view after each update. diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 3fb843b..c3df9c7 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, header_bar}, - ApplicationExt, Command, + ApplicationExt, Task, }; #[derive(Debug, Clone, PartialEq)] @@ -42,10 +42,10 @@ impl cosmic::Application for MultiWindow { &mut self.core } - fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, cosmic::app::Task) { let windows = MultiWindow { windows: HashMap::from([( - window::Id::MAIN, + self.core.main_window_id().unwrap(), Window { input_id: id::Id::new("main"), input_value: String::new(), @@ -54,12 +54,12 @@ impl cosmic::Application for MultiWindow { core, }; - (windows, cosmic::app::Command::none()) + (windows, cosmic::app::Task::none()) } fn subscription(&self) -> cosmic::iced_futures::Subscription { - event::listen_with(|event, _| { - if let iced::Event::Window(id, window_event) = event { + event::listen_with(|event, _, id| { + if let iced::Event::Window(window_event) = event { match window_event { window::Event::CloseRequested => Some(Message::CloseWindow(id)), window::Event::Opened { position, .. } => { @@ -77,18 +77,18 @@ impl cosmic::Application for MultiWindow { fn update( &mut self, message: Self::Message, - ) -> iced::Command> { + ) -> iced::Task> { match message { Message::CloseWindow(id) => window::close(id), Message::WindowClosed(id) => { self.windows.remove(&id); - Command::none() + Task::none() } Message::WindowOpened(id, ..) => { if let Some(window) = self.windows.get(&id) { text_input::focus(window.input_id.clone()) } else { - Command::none() + Task::none() } } Message::NewWindow => { @@ -113,13 +113,13 @@ impl cosmic::Application for MultiWindow { spawn_window } Message::Input(id, value) => { - if let Some(w) = self.windows.get_mut(&window::Id::MAIN) { + if let Some(w) = self.windows.get_mut(&self.core.main_window_id().unwrap()) { if id == w.input_id { w.input_value = value; } } - Command::none() + Task::none() } } } @@ -142,17 +142,15 @@ impl cosmic::Application for MultiWindow { column![input, new_window_button] .spacing(50) .width(Length::Fill) - .align_items(Alignment::Center), + .align_x(Alignment::Center), ); - let window_content = container(container(content).width(200).center_x()) + let window_content = container(container(content).center_x(Length::Fixed(200.))) .style(cosmic::style::Container::Background) - .width(Length::Fill) - .height(Length::Fill) - .center_x() - .center_y(); + .center_x(Length::Fill) + .center_y(Length::Fill); - if id == window::Id::MAIN { + if id == self.core.main_window_id().unwrap() { window_content.into() } else { column![header_bar().focused(focused), window_content].into() @@ -160,6 +158,6 @@ impl cosmic::Application for MultiWindow { } fn view(&self) -> cosmic::prelude::Element { - self.view_window(window::Id::MAIN) + self.view_window(self.core.main_window_id().unwrap()) } } diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 9882284..2c7b495 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Core, Settings, Task}; use cosmic::iced_core::Size; use cosmic::widget::{menu, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; @@ -106,7 +106,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, input: Self::Flags) -> (Self, Command) { + fn init(core: Core, input: Self::Flags) -> (Self, Task) { let mut nav_model = nav_bar::Model::default(); for (title, content) in input { @@ -143,13 +143,13 @@ impl cosmic::Application for App { } /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { self.nav_model.activate(id); self.update_title() } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::NavMenuAction(message) => match message { NavMenuAction::Delete(id) => self.nav_model.remove(id), @@ -168,7 +168,7 @@ impl cosmic::Application for App { }, } - Command::none() + Task::none() } /// Creates a view after each update. @@ -200,7 +200,7 @@ where .unwrap_or("Unknown Page") } - fn update_title(&mut self) -> Command { + fn update_title(&mut self) -> Task { let header_title = self.active_page_title().to_owned(); let window_title = format!("{header_title} — COSMIC AppDemo"); self.set_header_title(header_title); diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 5bd2a8b..f462f0b 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -4,7 +4,7 @@ //! An application which provides an open dialog use apply::Apply; -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Task, Core, Settings}; use cosmic::dialog::file_chooser::{self, FileFilter}; use cosmic::iced_core::Length; use cosmic::widget::button; @@ -66,7 +66,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { let mut app = App { core, file_contents: String::new(), @@ -77,7 +77,7 @@ impl cosmic::Application for App { app.set_header_title("Open a file".into()); let cmd = app.set_window_title( "COSMIC OpenDialog Demo".into(), - cosmic::iced::window::Id::MAIN, + cosmic::iced::self.core.main_window_id().unwrap(), ); (app, cmd) @@ -88,7 +88,7 @@ impl cosmic::Application for App { vec![button::suggested("Open").on_press(Message::OpenFile).into()] } - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::Cancelled => { eprintln!("open file dialog cancelled"); @@ -198,7 +198,7 @@ impl cosmic::Application for App { } } - Command::none() + Task::none() } fn view(&self) -> Element { diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs index 5140426..252992b 100644 --- a/examples/text-input/src/main.rs +++ b/examples/text-input/src/main.rs @@ -3,7 +3,7 @@ //! Application API example -use cosmic::app::{Command, Core, Settings}; +use cosmic::app::{Core, Settings, Task}; use cosmic::{executor, iced, ApplicationExt, Element}; /// Runs application with these settings @@ -55,7 +55,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { let mut app = App { core, editing: false, @@ -63,7 +63,7 @@ impl cosmic::Application for App { search_id: cosmic::widget::Id::unique(), }; - let commands = Command::batch(vec![ + let commands = Task::batch(vec![ cosmic::widget::text_input::focus(app.search_id.clone()), app.update_title(), ]); @@ -72,7 +72,7 @@ impl cosmic::Application for App { } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Command { + fn update(&mut self, message: Self::Message) -> Task { match message { Message::Input(text) => { self.input = text; @@ -83,7 +83,7 @@ impl cosmic::Application for App { } } - Command::none() + Task::none() } /// Creates a view after each update. @@ -115,7 +115,7 @@ impl App where Self: cosmic::Application, { - fn update_title(&mut self) -> Command { + fn update_title(&mut self) -> Task { let window_title = format!("COSMIC TextInputs Demo"); self.set_header_title(window_title.clone()); self.set_window_title(window_title) diff --git a/iced b/iced index 0619950..f2f9dfc 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 061995084a5775b4fd7df63dda336be01ddf491c +Subproject commit f2f9dfc6c37e14c4f8ff5dafe70d72d3c40eb692 diff --git a/src/app/command.rs b/src/app/command.rs index 80682d1..5fd77fc 100644 --- a/src/app/command.rs +++ b/src/app/command.rs @@ -7,62 +7,88 @@ use iced::window; use super::Message; /// Commands for COSMIC applications. -pub type Command = iced::Command>; +pub type Task = iced::Task>; /// Creates a command which yields a [`crate::app::Message`]. -pub fn message(message: Message) -> Command { +pub fn message(message: Message) -> Task { crate::command::message(message) } /// Convenience methods for building message-based commands. pub mod message { /// Creates a command which yields an application message. - pub fn app(message: M) -> crate::app::Command { + pub fn app(message: M) -> crate::app::Task { super::message(super::Message::App(message)) } /// Creates a command which yields a cosmic message. - pub fn cosmic( - message: crate::app::cosmic::Message, - ) -> crate::app::Command { + pub fn cosmic(message: crate::app::cosmic::Message) -> crate::app::Task { super::message(super::Message::Cosmic(message)) } } -pub fn drag(id: Option) -> iced::Command> { - crate::command::drag(id).map(Message::Cosmic) +impl crate::app::Core { + pub fn drag(&self, id: Option) -> iced::Task> { + let Some(id) = id.or(self.main_window.get().cloned()) else { + return iced::Task::none(); + }; + crate::command::drag(id).map(Message::Cosmic) + } + + pub fn maximize( + &self, + id: Option, + maximized: bool, + ) -> iced::Task> { + let Some(id) = id.or(self.main_window.get().cloned()) else { + return iced::Task::none(); + }; + crate::command::maximize(id, maximized).map(Message::Cosmic) + } + + pub fn minimize(&self, id: Option) -> iced::Task> { + let Some(id) = id.or(self.main_window.get().cloned()) else { + return iced::Task::none(); + }; + crate::command::minimize(id).map(Message::Cosmic) + } + + pub fn set_scaling_factor(&self, factor: f32) -> iced::Task> { + message::cosmic(super::cosmic::Message::ScaleFactor(factor)) + } + + pub fn set_title( + &self, + id: Option, + title: String, + ) -> iced::Task> { + let Some(id) = id.or(self.main_window.get().cloned()) else { + return iced::Task::none(); + }; + crate::command::set_title(id, title).map(Message::Cosmic) + } + + pub fn set_windowed( + &self, + id: Option, + ) -> iced::Task> { + let Some(id) = id.or(self.main_window.get().cloned()) else { + return iced::Task::none(); + }; + crate::command::set_windowed(id).map(Message::Cosmic) + } + + pub fn toggle_maximize( + &self, + id: Option, + ) -> iced::Task> { + let Some(id) = id.or(self.main_window.get().cloned()) else { + return iced::Task::none(); + }; + crate::command::toggle_maximize(id).map(Message::Cosmic) + } } -pub fn maximize( - id: Option, - maximized: bool, -) -> iced::Command> { - crate::command::maximize(id, maximized).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> { - message::cosmic(super::cosmic::Message::ScaleFactor(factor)) -} - -pub fn set_theme(theme: crate::Theme) -> iced::Command> { +pub fn set_theme(theme: crate::Theme) -> iced::Task> { message::cosmic(super::cosmic::Message::AppThemeChange(theme)) } - -pub fn set_title( - id: Option, - title: String, -) -> iced::Command> { - crate::command::set_title(id, title).map(Message::Cosmic) -} - -pub fn set_windowed(id: Option) -> iced::Command> { - crate::command::set_windowed(id).map(Message::Cosmic) -} - -pub fn toggle_maximize(id: Option) -> iced::Command> { - crate::command::toggle_maximize(id).map(Message::Cosmic) -} diff --git a/src/app/core.rs b/src/app/core.rs index eee5ae5..f1e63d7 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use std::collections::HashMap; +use std::{cell::OnceCell, collections::HashMap}; use crate::widget::nav_bar; use cosmic_config::CosmicConfigEntry; @@ -40,8 +40,8 @@ pub struct Window { pub show_close: bool, pub show_maximize: bool, pub show_minimize: bool, - height: u32, - width: u32, + height: f32, + width: f32, } /// COSMIC-specific application settings @@ -93,6 +93,10 @@ pub struct Core { #[cfg(feature = "dbus-config")] pub(crate) settings_daemon: Option>, + + pub(crate) main_window: OnceCell, + + pub(crate) exit_on_main_window_closed: bool, } impl Default for Core { @@ -135,10 +139,10 @@ impl Default for Core { show_maximize: true, show_minimize: true, show_window_menu: false, - height: 0, - width: 0, + height: 0., + width: 0., }, - focused_window: Some(window::Id::MAIN), + focused_window: None, #[cfg(feature = "applet")] applet: crate::applet::Context::default(), #[cfg(feature = "single-instance")] @@ -148,6 +152,8 @@ impl Default for Core { portal_is_dark: None, portal_accent: None, portal_is_high_contrast: None, + main_window: OnceCell::new(), + exit_on_main_window_closed: true, } } } @@ -297,12 +303,12 @@ impl Core { } /// Set the height of the main window. - pub(crate) fn set_window_height(&mut self, new_height: u32) { + pub(crate) fn set_window_height(&mut self, new_height: f32) { self.window.height = new_height; } /// Set the width of the main window. - pub(crate) fn set_window_width(&mut self, new_width: u32) { + pub(crate) fn set_window_width(&mut self, new_width: f32) { self.window.width = new_width; self.is_condensed_update(); } @@ -364,4 +370,12 @@ impl Core { self.portal_is_dark .unwrap_or(self.system_theme_mode.is_dark) } + + /// The [`Id`] of the main window + pub fn main_window_id(&self) -> Option { + self.main_window + .get() + .filter(|id| iced::window::Id::NONE != **id) + .cloned() + } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 8b43769..295a005 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1,32 +1,23 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +use std::borrow::Borrow; use std::sync::Arc; -use super::{command, Application, ApplicationExt, Core, Subscription}; +use super::{Application, ApplicationExt, Core, Subscription}; use crate::config::CosmicTk; -use crate::theme::{self, Theme, ThemeType, THEME}; +use crate::theme::{Theme, ThemeType, THEME}; use crate::widget::nav_bar; use crate::{keyboard_nav, Element}; #[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; #[cfg(feature = "wayland")] -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::event::wayland; #[cfg(not(any(feature = "multi-window", feature = "wayland")))] use iced::Application as IcedApplication; -use iced::{window, Command}; +use iced::{window, Task}; use iced_futures::event::listen_with; -#[cfg(not(feature = "wayland"))] -use iced_runtime::command::Action; -#[cfg(not(feature = "wayland"))] -use iced_runtime::window::Action as WindowAction; use palette::color_difference::EuclideanDistance; /// A message managed internally by COSMIC. @@ -65,7 +56,7 @@ pub enum Message { /// Updates the window maximized state WindowMaximized(window::Id, bool), /// Updates the tracked window geometry. - WindowResize(window::Id, u32, u32), + WindowResize(window::Id, f32, f32), /// Tracks updates to window state. #[cfg(feature = "wayland")] WindowState(window::Id, WindowState), @@ -86,7 +77,9 @@ pub enum Message { Unfocus(window::Id), /// Tracks updates to window suggested size. #[cfg(feature = "applet")] - Configure(cctk::sctk::shell::xdg::window::WindowConfigure), + SuggestedBounds(Option), + /// Window Created + MainWindowCreated(window::Id), } #[derive(Default)] @@ -94,109 +87,104 @@ pub struct Cosmic { pub app: App, } -impl IcedApplication for Cosmic +impl Cosmic where T::Message: Send + 'static, { - type Executor = T::Executor; - type Flags = (Core, T::Flags); - type Message = super::Message; - type Theme = Theme; - - fn new((mut core, flags): Self::Flags) -> (Self, iced::Command) { + pub fn init( + (mut core, flags, window_settings): (Core, T::Flags, iced::window::Settings), + ) -> (Self, iced::Task>) { #[cfg(feature = "dbus-config")] { use iced_futures::futures::executor::block_on; core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); } - let (model, command) = T::init(core, flags); + let (model, mut command) = T::init(core, flags); (Self::new(model), command) } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] - fn title(&self) -> String { + #[cfg(not(feature = "multi-window"))] + pub fn title(&self) -> String { self.app.title().to_string() } - #[cfg(any(feature = "multi-window", feature = "wayland"))] - fn title(&self, id: window::Id) -> String { + #[cfg(feature = "multi-window")] + pub fn title(&self, id: window::Id) -> String { self.app.title(id).to_string() } - fn update(&mut self, message: Self::Message) -> iced::Command { + pub fn update( + &mut self, + message: super::Message, + ) -> iced::Task> { match message { super::Message::App(message) => self.app.update(message), super::Message::Cosmic(message) => self.cosmic_update(message), - super::Message::None => iced::Command::none(), + super::Message::None => iced::Task::none(), #[cfg(feature = "single-instance")] super::Message::DbusActivation(message) => self.app.dbus_activation(message), } } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] - fn scale_factor(&self) -> f64 { + #[cfg(not(feature = "multi-window"))] + pub fn scale_factor(&self) -> f64 { f64::from(self.app.core().scale_factor()) } - #[cfg(any(feature = "multi-window", feature = "wayland"))] - fn scale_factor(&self, _id: window::Id) -> f64 { + #[cfg(feature = "multi-window")] + pub fn scale_factor(&self, _id: window::Id) -> f64 { f64::from(self.app.core().scale_factor()) } - fn style(&self) -> ::Style { + pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance { if let Some(style) = self.app.style() { style } else if self.app.core().window.sharp_corners { - theme::Application::default() + let theme = THEME.lock().unwrap(); + crate::style::iced::application::appearance(theme.borrow()) } else { - theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance { + let theme = THEME.lock().unwrap(); + iced_runtime::Appearance { background_color: iced_core::Color::TRANSPARENT, icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), - })) + } } } #[allow(clippy::too_many_lines)] - fn subscription(&self) -> Subscription { - let window_events = listen_with(|event, _| { + pub fn subscription(&self) -> Subscription> { + let window_events = listen_with(|event, _, id| { match event { - iced::Event::Window(id, window::Event::Resized { width, height }) => { + iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => { return Some(Message::WindowResize(id, width, height)); } - iced::Event::Window(id, window::Event::Closed) => { + iced::Event::Window(window::Event::Closed) => { return Some(Message::SurfaceClosed(id)) } - iced::Event::Window(id, window::Event::Focused) => return Some(Message::Focus(id)), - iced::Event::Window(id, window::Event::Unfocused) => { - return Some(Message::Unfocus(id)) + iced::Event::Window(window::Event::Focused) => return Some(Message::Focus(id)), + iced::Event::Window(window::Event::Unfocused) => return Some(Message::Unfocus(id)), + iced::Event::Window(window::Event::Opened { .. }) => { + return Some(Message::MainWindowCreated(id)); } #[cfg(feature = "wayland")] - iced::Event::PlatformSpecific(PlatformSpecific::Wayland(event)) => match event { - wayland::Event::Window(WindowEvent::State(state), _surface, id) => { - return Some(Message::WindowState(id, state)); + iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { + match event { + wayland::Event::Popup(wayland::PopupEvent::Done, _, id) + | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => { + return Some(Message::SurfaceClosed(id)); + } + #[cfg(feature = "applet")] + wayland::Event::Window( + iced::event::wayland::WindowEvent::SuggestedBounds(b), + ) => { + return Some(Message::SuggestedBounds(b)); + } + _ => (), } - wayland::Event::Window( - WindowEvent::WmCapabilities(capabilities), - _surface, - id, - ) => { - return Some(Message::WmCapabilities(id, capabilities)); - } - wayland::Event::Popup(wayland::PopupEvent::Done, _, id) - | wayland::Event::Layer(wayland::LayerEvent::Done, _, id) => { - return Some(Message::SurfaceClosed(id)); - } - #[cfg(feature = "applet")] - wayland::Event::Window(WindowEvent::Configure(conf), _surface, id) - if id == window::Id::MAIN => - { - return Some(Message::Configure(conf)); - } - _ => (), - }, + } _ => (), } @@ -275,19 +263,24 @@ where Subscription::batch(subscriptions) } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] - fn theme(&self) -> Self::Theme { + #[cfg(not(feature = "multi-window"))] + pub fn theme(&self) -> Theme { crate::theme::active() } - #[cfg(any(feature = "multi-window", feature = "wayland"))] - fn theme(&self, _id: window::Id) -> Self::Theme { + #[cfg(feature = "multi-window")] + pub fn theme(&self, _id: window::Id) -> Theme { crate::theme::active() } - #[cfg(any(feature = "multi-window", feature = "wayland"))] - fn view(&self, id: window::Id) -> Element { - if id != self.app.main_window_id() { + #[cfg(feature = "multi-window")] + pub fn view(&self, id: window::Id) -> Element> { + if !self + .app + .core() + .main_window_id() + .is_some_and(|main_id| main_id == id) + { return self.app.view_window(id).map(super::Message::App); } @@ -298,37 +291,43 @@ where } } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] - fn view(&self) -> Element { + #[cfg(not(feature = "multi-window"))] + pub fn view(&self) -> Element> { self.app.view_main() } } impl Cosmic { - #[cfg(feature = "wayland")] - pub fn close(&mut self) -> iced::Command> { - iced_sctk::commands::window::close_window(self.app.main_window_id()) - } - - #[cfg(not(feature = "wayland"))] #[allow(clippy::unused_self)] - pub fn close(&mut self) -> iced::Command> { - iced::Command::single(Action::Window(WindowAction::Close( - self.app.main_window_id(), - ))) + pub fn close(&mut self) -> iced::Task> { + if let Some(id) = self.app.core().main_window_id() { + iced::window::close(id) + } else { + iced::Task::none() + } } #[allow(clippy::too_many_lines)] - fn cosmic_update(&mut self, message: Message) -> iced::Command> { + fn cosmic_update(&mut self, message: Message) -> iced::Task> { match message { Message::WindowMaximized(id, maximized) => { - if self.app.main_window_id() == id { + if self + .app + .core() + .main_window_id() + .is_some_and(|main_id| main_id == id) + { self.app.core_mut().window.sharp_corners = maximized; } } Message::WindowResize(id, width, height) => { - if self.app.main_window_id() == id { + if self + .app + .core() + .main_window_id() + .is_some_and(|main_id| main_id == id) + { self.app.core_mut().set_window_width(width); self.app.core_mut().set_window_height(height); } @@ -336,15 +335,19 @@ impl Cosmic { self.app.on_window_resize(id, width, height); //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) - #[cfg(not(feature = "wayland"))] - return iced::window::fetch_maximized(id, move |maximized| { + return iced::window::get_maximized(id).map(move |maximized| { super::Message::Cosmic(Message::WindowMaximized(id, maximized)) }); } #[cfg(feature = "wayland")] Message::WindowState(id, state) => { - if self.app.main_window_id() == id { + if self + .app + .core() + .main_window_id() + .is_some_and(|main_id| main_id == id) + { self.app.core_mut().window.sharp_corners = state.intersects( WindowState::MAXIMIZED | WindowState::FULLSCREEN @@ -359,7 +362,12 @@ impl Cosmic { #[cfg(feature = "wayland")] Message::WmCapabilities(id, capabilities) => { - if self.app.main_window_id() == id { + if self + .app + .core() + .main_window_id() + .is_some_and(|main_id| main_id == id) + { self.app.core_mut().window.show_maximize = capabilities.contains(WindowManagerCapabilities::MAXIMIZE); self.app.core_mut().window.show_minimize = @@ -379,9 +387,7 @@ 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_maximize(Some(self.app.main_window_id())) - } + keyboard_nav::Message::Fullscreen => return self.app.core().toggle_maximize(None), }, Message::ContextDrawer(show) => { @@ -389,11 +395,11 @@ impl Cosmic { return self.app.on_context_drawer(); } - Message::Drag => return command::drag(Some(self.app.main_window_id())), + Message::Drag => return self.app.core().drag(None), - Message::Minimize => return command::minimize(Some(self.app.main_window_id())), + Message::Minimize => return self.app.core().minimize(None), - Message::Maximize => return command::toggle_maximize(Some(self.app.main_window_id())), + Message::Maximize => return self.app.core().toggle_maximize(None), Message::NavBar(key) => { self.app.core_mut().nav_bar_set_toggled_condensed(false); @@ -433,7 +439,7 @@ impl Cosmic { let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark(); // Ignore updates if the current theme mode does not match. if cur_is_dark != theme.cosmic().is_dark { - return iced::Command::none(); + return iced::Task::none(); } let cmd = self.app.system_theme_update(&keys, theme.cosmic()); // Record the last-known system theme in event that the current theme is custom. @@ -479,7 +485,7 @@ impl Cosmic { } Message::SystemThemeModeChange(keys, mode) => { if !keys.contains(&"is_dark") { - return iced::Command::none(); + return iced::Task::none(); } if match THEME.lock().unwrap().theme_type { ThemeType::System { @@ -488,7 +494,7 @@ impl Cosmic { } => prefer_dark.is_some(), _ => false, } { - return iced::Command::none(); + return iced::Task::none(); } let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; @@ -527,23 +533,37 @@ impl Cosmic { } } } - return Command::batch(cmds); + return Task::batch(cmds); } - Message::Activate(_token) => { + Message::Activate(_token) => + { #[cfg(feature = "wayland")] - return iced_sctk::commands::activation::activate( - self.app.main_window_id(), - #[allow(clippy::used_underscore_binding)] - _token, - ); - } - Message::SurfaceClosed(id) => { - if let Some(msg) = self.app.on_close_requested(id) { - return self.app.update(msg); + if let Some(id) = self.app.core().main_window_id() { + return iced_winit::platform_specific::commands::activation::activate( + id, + #[allow(clippy::used_underscore_binding)] + _token, + ); } } + Message::SurfaceClosed(id) => { + let mut ret = if let Some(msg) = self.app.on_close_requested(id) { + self.app.update(msg) + } else { + Task::none() + }; + let core = self.app.core(); + if core.exit_on_main_window_closed + && core.main_window_id().is_some_and(|m_id| id == m_id) + { + ret = Task::batch(vec![iced::exit::>()]); + } + return ret; + } Message::ShowWindowMenu => { - return window::show_window_menu(window::Id::MAIN); + if let Some(id) = self.app.core().main_window_id() { + return iced::window::show_system_menu(id); + } } #[cfg(feature = "xdg-portal")] Message::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { @@ -555,7 +575,7 @@ impl Cosmic { } => prefer_dark.is_some(), _ => false, } { - return iced::Command::none(); + return iced::Task::none(); } let is_dark = match s { ColorScheme::NoPreference => None, @@ -595,7 +615,7 @@ impl Cosmic { if cur_accent.distance_squared(*c) < 0.00001 { // skip calculations if we already have the same color - return iced::Command::none(); + return iced::Task::none(); } { @@ -640,19 +660,20 @@ impl Cosmic { core.focused_window = None; } } - #[cfg(feature = "applet")] - Message::Configure(configure) => { - if let Some(w) = configure.new_size.0 { - self.app.core_mut().set_window_width(w.get()); - } - if let Some(h) = configure.new_size.1 { - self.app.core_mut().set_window_height(h.get()); - } - self.app.core_mut().applet.configure = Some(configure); + Message::MainWindowCreated(id) => { + let core = self.app.core_mut(); + _ = core.main_window.set(id); } + #[cfg(feature = "applet")] + Message::SuggestedBounds(b) => { + tracing::info!("Suggested bounds: {b:?}"); + let core = self.app.core_mut(); + core.applet.suggested_bounds = b; + } + _ => {} } - iced::Command::none() + iced::Task::none() } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 23f9ff2..5d46d54 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -9,6 +9,8 @@ pub mod command; mod core; pub mod cosmic; +#[cfg(all(feature = "winit", feature = "multi-window"))] +pub(crate) mod multi_window; pub mod settings; pub mod message { @@ -47,17 +49,14 @@ pub mod message { use std::borrow::Cow; -pub use self::command::Command; +pub use self::command::Task; pub use self::core::Core; pub use self::settings::Settings; use crate::prelude::*; use crate::theme::THEME; use crate::widget::{context_drawer, horizontal_space, id_container, menu, nav_bar, popover}; use apply::Apply; -#[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}; +use iced::window; use iced::{Length, Subscription}; pub use message::Message; use url::Url; @@ -73,15 +72,15 @@ use { pub(crate) fn iced_settings( settings: Settings, flags: App::Flags, -) -> iced::Settings<(Core, App::Flags)> { +) -> (iced::Settings, (Core, App::Flags, iced::window::Settings)) { preload_fonts(); let mut core = Core::default(); core.debug = settings.debug; core.icon_theme_override = settings.default_icon_theme.is_some(); core.set_scale_factor(settings.scale_factor); - core.set_window_width(settings.size.width as u32); - core.set_window_height(settings.size.height as u32); + core.set_window_width(settings.size.width); + core.set_window_height(settings.size.height); if let Some(icon_theme) = settings.default_icon_theme { crate::icon_theme::set_default(icon_theme); @@ -91,65 +90,40 @@ pub(crate) fn iced_settings( THEME.lock().unwrap().set_theme(settings.theme.theme_type); - let mut iced = iced::Settings::with_flags((core, flags)); + if settings.no_main_window { + core.main_window.set(iced::window::Id::NONE).unwrap(); + } + + let mut iced = iced::Settings::default(); iced.antialiasing = settings.antialiasing; iced.default_font = settings.default_font; iced.default_text_size = iced::Pixels(settings.default_text_size); - iced.exit_on_close_request = settings.exit_on_close; - #[cfg(not(feature = "wayland"))] - { - let exit_on_close = settings.exit_on_close; - iced.window.exit_on_close_request = exit_on_close; - } + let exit_on_close = settings.exit_on_close; + iced.exit_on_close_request = exit_on_close; + let mut window_settings = iced::window::Settings::default(); + window_settings.exit_on_close_request = 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(); + window_settings.platform_specific.application_id = App::APP_ID.to_string(); + core.exit_on_main_window_closed = exit_on_close; + + if let Some(border_size) = settings.resizable { + window_settings.resize_border = border_size as u32; + window_settings.resizable = true; + } + window_settings.decorations = !settings.client_decorations; + window_settings.size = settings.size; + let min_size = settings.size_limits.min(); + if min_size != iced::Size::ZERO { + window_settings.min_size = Some(min_size); + } + let max_size = settings.size_limits.max(); + if max_size != iced::Size::INFINITY { + window_settings.max_size = Some(max_size); } - #[cfg(feature = "wayland")] - { - use iced::wayland::actions::window::SctkWindowSettings; - use iced_sctk::settings::InitialSurface; - iced.initial_surface = if settings.no_main_window { - InitialSurface::None - } else { - InitialSurface::XdgWindow(SctkWindowSettings { - app_id: Some(App::APP_ID.to_owned()), - autosize: settings.autosize, - client_decorations: settings.client_decorations, - resizable: settings.resizable, - size: (settings.size.width as u32, settings.size.height as u32).into(), - size_limits: settings.size_limits, - title: None, - transparent: settings.transparent, - xdg_activation_token: std::env::var("XDG_ACTIVATION_TOKEN").ok(), - ..SctkWindowSettings::default() - }) - }; - } - - #[cfg(not(feature = "wayland"))] - { - if let Some(border_size) = settings.resizable { - iced.window.resize_border = border_size as u32; - iced.window.resizable = true; - } - 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; - } - - iced + window_settings.transparent = settings.transparent; + (iced, (core, flags, window_settings)) } /// Launch a COSMIC application with the given [`Settings`]. @@ -158,9 +132,40 @@ pub(crate) fn iced_settings( /// /// Returns error on application failure. pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { - let settings = iced_settings::(settings, flags); - - cosmic::Cosmic::::run(settings) + let default_font = settings.default_font; + let (settings, flags) = iced_settings::(settings, flags); + #[cfg(not(feature = "multi-window"))] + { + iced::application( + cosmic::Cosmic::title, + cosmic::Cosmic::update, + cosmic::Cosmic::view, + ) + .subscription(cosmic::Cosmic::subscription) + .style(cosmic::Cosmic::style) + .theme(cosmic::Cosmic::theme) + .window_size((500.0, 800.0)) + .settings(settings) + .window(flags.2.clone()) + .run_with(move || cosmic::Cosmic::::init(flags)) + } + #[cfg(feature = "multi-window")] + { + let mut app = multi_window::multi_window( + cosmic::Cosmic::title, + cosmic::Cosmic::update, + cosmic::Cosmic::view, + ); + if flags.0.main_window.get().is_none() { + app = app.window(flags.2.clone()); + _ = flags.0.main_window.set(iced_core::window::Id::RESERVED); + } + app.subscription(cosmic::Cosmic::subscription) + .style(cosmic::Cosmic::style) + .theme(cosmic::Cosmic::theme) + .settings(settings) + .run_with(move || cosmic::Cosmic::::init(flags)) + } } #[cfg(feature = "single-instance")] @@ -362,9 +367,41 @@ where tracing::info!("Another instance is running"); Ok(()) } else { - let mut settings = iced_settings::(settings, flags); - settings.flags.0.single_instance = true; - cosmic::Cosmic::::run(settings) + let (settings, mut flags) = iced_settings::(settings, flags); + flags.0.single_instance = true; + + #[cfg(not(feature = "multi-window"))] + { + iced::application( + cosmic::Cosmic::title, + cosmic::Cosmic::update, + cosmic::Cosmic::view, + ) + .subscription(cosmic::Cosmic::subscription) + .style(cosmic::Cosmic::style) + .theme(cosmic::Cosmic::theme) + .window_size((500.0, 800.0)) + .settings(settings) + .window(flags.2.clone()) + .run_with(move || cosmic::Cosmic::::init(flags)) + } + #[cfg(feature = "multi-window")] + { + let mut app = multi_window::multi_window( + cosmic::Cosmic::title, + cosmic::Cosmic::update, + cosmic::Cosmic::view, + ); + if flags.0.main_window.get().is_none() { + app = app.window(flags.2.clone()); + _ = flags.0.main_window.set(iced_core::window::Id::RESERVED); + } + app.subscription(cosmic::Cosmic::subscription) + .style(cosmic::Cosmic::style) + .theme(cosmic::Cosmic::theme) + .settings(settings) + .run_with(move || cosmic::Cosmic::::init(flags)) + } } } @@ -409,7 +446,7 @@ where fn core_mut(&mut self) -> &mut Core; /// Creates the application, and optionally emits command on initialize. - fn init(core: Core, flags: Self::Flags) -> (Self, Command); + fn init(core: Core, flags: Self::Flags) -> (Self, Task); /// Displays a context drawer on the side of the application window when `Some`. fn context_drawer(&self) -> Option> { @@ -441,11 +478,6 @@ where Vec::new() } - /// Get the main [`window::Id`], which is [`window::Id::MAIN`] by default - fn main_window_id(&self) -> window::Id { - window::Id::MAIN - } - /// Allows overriding the default nav bar widget. fn nav_bar(&self) -> Option>> { if !self.core().nav_bar_active() { @@ -491,32 +523,32 @@ where } // Called when context drawer is toggled - fn on_context_drawer(&mut self) -> Command { - Command::none() + fn on_context_drawer(&mut self) -> Task { + Task::none() } /// Called when the escape key is pressed. - fn on_escape(&mut self) -> Command { - Command::none() + fn on_escape(&mut self) -> Task { + Task::none() } /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { - Command::none() + fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { + Task::none() } /// Called when a context menu is requested for a navigation item. - fn on_nav_context(&mut self, id: nav_bar::Id) -> Command { - Command::none() + fn on_nav_context(&mut self, id: nav_bar::Id) -> Task { + Task::none() } /// Called when the search function is requested. - fn on_search(&mut self) -> Command { - Command::none() + fn on_search(&mut self) -> Task { + Task::none() } /// Called when a window is resized. - fn on_window_resize(&mut self, id: window::Id, width: u32, height: u32) {} + fn on_window_resize(&mut self, id: window::Id, width: f32, height: f32) {} /// Event sources that are to be listened to. fn subscription(&self) -> Subscription { @@ -524,8 +556,8 @@ where } /// Respond to an application-specific message. - fn update(&mut self, message: Self::Message) -> Command { - Command::none() + fn update(&mut self, message: Self::Message) -> Task { + Task::none() } /// Respond to a system theme change @@ -533,8 +565,8 @@ where &mut self, keys: &[&'static str], new_theme: &cosmic_theme::Theme, - ) -> Command { - Command::none() + ) -> Task { + Task::none() } /// Respond to a system theme mode change @@ -542,8 +574,8 @@ where &mut self, keys: &[&'static str], new_theme: &cosmic_theme::ThemeMode, - ) -> Command { - Command::none() + ) -> Task { + Task::none() } /// Constructs the view for the main window. @@ -555,33 +587,33 @@ where } /// Overrides the default style for applications - fn style(&self) -> Option<::Style> { + fn style(&self) -> Option { None } /// Handles dbus activation messages #[cfg(feature = "single-instance")] - fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Command { - Command::none() + fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Task { + Task::none() } } /// Methods automatically derived for all types implementing [`Application`]. pub trait ApplicationExt: Application { /// Initiates a window drag. - fn drag(&mut self) -> Command; + fn drag(&mut self) -> Task; /// Maximizes the window. - fn maximize(&mut self) -> Command; + fn maximize(&mut self) -> Task; /// Minimizes the window. - fn minimize(&mut self) -> Command; + fn minimize(&mut self) -> Task; /// Get the title of the main window. - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] + #[cfg(not(feature = "multi-window"))] fn title(&self) -> &str; - #[cfg(any(feature = "multi-window", feature = "wayland"))] + #[cfg(feature = "multi-window")] /// Get the title of a window. fn title(&self, id: window::Id) -> &str; @@ -600,56 +632,58 @@ pub trait ApplicationExt: Application { self.core_mut().set_header_title(title); } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] + #[cfg(not(feature = "multi-window"))] /// Set the title of the main window. - fn set_window_title(&mut self, title: String) -> Command; + fn set_window_title(&mut self, title: String) -> Task; - #[cfg(any(feature = "multi-window", feature = "wayland"))] + #[cfg(feature = "multi-window")] /// Set the title of a window. - fn set_window_title(&mut self, title: String, id: window::Id) -> Command; + fn set_window_title(&mut self, title: String, id: window::Id) -> Task; /// View template for the main window. fn view_main(&self) -> Element>; } impl ApplicationExt for App { - fn drag(&mut self) -> Command { - command::drag(Some(self.main_window_id())) + fn drag(&mut self) -> Task { + self.core().drag(None) } - fn maximize(&mut self) -> Command { - command::maximize(Some(self.main_window_id()), true) + fn maximize(&mut self) -> Task { + self.core().maximize(None, true) } - fn minimize(&mut self) -> Command { - command::minimize(Some(self.main_window_id())) + fn minimize(&mut self) -> Task { + self.core().minimize(None) } - #[cfg(any(feature = "multi-window", feature = "wayland"))] + #[cfg(feature = "multi-window")] fn title(&self, id: window::Id) -> &str { self.core().title.get(&id).map_or("", |s| s.as_str()) } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] + #[cfg(not(feature = "multi-window"))] fn title(&self) -> &str { self.core() - .title - .get(&self.main_window_id()) - .map_or("", |s| s.as_str()) + .main_window_id() + .and_then(|id| self.core().title.get(&id).map(std::string::String::as_str)) + .unwrap_or("") } - #[cfg(any(feature = "multi-window", feature = "wayland"))] - fn set_window_title(&mut self, title: String, id: window::Id) -> Command { + #[cfg(feature = "multi-window")] + fn set_window_title(&mut self, title: String, id: window::Id) -> Task { self.core_mut().title.insert(id, title.clone()); - command::set_title(Some(id), title) + self.core().set_title(Some(id), title) } - #[cfg(not(any(feature = "multi-window", feature = "wayland")))] - fn set_window_title(&mut self, title: String) -> Command { - let id = self.main_window_id(); + #[cfg(not(feature = "multi-window"))] + fn set_window_title(&mut self, title: String) -> Task { + let Some(id) = self.core().main_window_id() else { + return Task::none(); + }; self.core_mut().title.insert(id, title.clone()); - Command::none() + Task::none() } #[allow(clippy::too_many_lines)] @@ -659,7 +693,7 @@ impl ApplicationExt for App { let is_condensed = core.is_condensed(); let focused = core .focused_window() - .is_some_and(|i| i == self.main_window_id()); + .is_some_and(|i| Some(i) == self.core().main_window_id()); let content_row = crate::widget::row::with_children({ let mut widgets = Vec::with_capacity(4); @@ -678,7 +712,7 @@ impl ApplicationExt for App { if self.nav_model().is_none() || core.show_content() { // Manual spacing must be used due to state workarounds below if has_nav { - widgets.push(horizontal_space(Length::Fixed(8.0)).into()); + widgets.push(horizontal_space().width(Length::Fixed(8.0)).into()); } let main_content = self.view().map(Message::App); @@ -727,7 +761,7 @@ impl ApplicationExt for App { ); } else { //TODO: this element is added to workaround state issues - widgets.push(horizontal_space(Length::Shrink).into()); + widgets.push(horizontal_space().width(Length::Shrink).into()); } } } @@ -744,7 +778,7 @@ impl ApplicationExt for App { .padding([0, 8, 8, 8]) .width(iced::Length::Fill) .height(iced::Length::Fill) - .style(crate::theme::Container::WindowBackground) + .class(crate::theme::Container::WindowBackground) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { @@ -770,7 +804,7 @@ impl ApplicationExt for App { } else { Message::Cosmic(cosmic::Message::ToggleNavBar) }) - .style(crate::theme::Button::HeaderBar); + .class(crate::theme::Button::HeaderBar); header = header.start(toggle); } @@ -825,11 +859,9 @@ impl ApplicationExt for App { #[cfg(feature = "single-instance")] fn single_instance_subscription() -> Subscription> { use iced_futures::futures::StreamExt; - - iced::subscription::channel( + iced_futures::Subscription::run_with_id( TypeId::of::(), - 10, - move |mut output| async move { + iced::stream::channel(10, move |mut output| async move { let mut single_instance: DbusActivation = DbusActivation::new(); let mut rx = single_instance.rx(); if let Ok(builder) = zbus::ConnectionBuilder::session() { @@ -888,7 +920,7 @@ fn single_instance_subscription() -> Subscription { + update: Update, + view: View, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, +} + +/// Creates an iced [`MultiWindow`] given its title, update, and view logic. +pub fn multi_window( + title: impl Title, + update: impl application::Update, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> MultiWindow> +where + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, +{ + use std::marker::PhantomData; + + impl Program + for Instance + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + DefaultStyle, + Renderer: program::Renderer, + Update: application::Update, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; + + fn update(&self, state: &mut Self::State, message: Self::Message) -> Task { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state, window).into() + } + } + + MultiWindow { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + window: None, + } + .title(title) +} + +/// The underlying definition and configuration of an iced daemon. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create a [`MultiWindow`] with the [`daemon`] helper. +#[derive(Debug)] +pub struct MultiWindow { + raw: P, + settings: Settings, + window: Option, +} + +impl MultiWindow

{ + #[cfg(any(feature = "winit", feature = "wayland"))] + /// Runs the [`MultiWindow`]. + /// + /// The state of the [`MultiWindow`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.raw.run(self.settings, self.window) + } + + #[cfg(any(feature = "winit", feature = "wayland"))] + /// Runs the [`MultiWindow`] with a closure that creates the initial state. + pub fn run_with(self, initialize: I) -> Result + where + Self: 'static, + I: FnOnce() -> (P::State, Task) + 'static, + { + self.raw.run_with(self.settings, self.window, initialize) + } + + /// Sets the [`Settings`] that will be used to run the [`MultiWindow`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Title`] of the [`MultiWindow`]. + pub(crate) fn title( + self, + title: impl Title, + ) -> MultiWindow> { + MultiWindow { + raw: with_title(self.raw, move |state, window| title.title(state, window)), + settings: self.settings, + window: self.window, + } + } + + /// Sets the subscription logic of the [`MultiWindow`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription, + ) -> MultiWindow> { + MultiWindow { + raw: with_subscription(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the theme logic of the [`MultiWindow`]. + pub fn theme( + self, + f: impl Fn(&P::State, window::Id) -> P::Theme, + ) -> MultiWindow> { + MultiWindow { + raw: with_theme(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the style logic of the [`MultiWindow`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> Appearance, + ) -> MultiWindow> { + MultiWindow { + raw: with_style(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the window settings of the [`MultiWindow`]. + pub fn window(self, window: window::Settings) -> Self { + Self { + raw: self.raw, + settings: self.settings, + window: Some(window), + } + } +} + +/// The title logic of some [`MultiWindow`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State, window::Id) -> String`. +/// +/// This trait allows the [`daemon`] builder to take any of them. +pub trait Title { + /// Produces the title of the [`MultiWindow`]. + fn title(&self, state: &State, window: window::Id) -> String; +} + +impl Title for &'static str { + fn title(&self, _state: &State, _window: window::Id) -> String { + (*self).to_string() + } +} + +impl Title for T +where + T: Fn(&State, window::Id) -> String, +{ + fn title(&self, state: &State, window: window::Id) -> String { + self(state, window) + } +} + +/// The view logic of some [`MultiWindow`]. +/// +/// This trait allows the [`daemon`] builder to take any closure that +/// returns any `Into>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`MultiWindow`]. + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State, window::Id) -> Widget, + State: 'static, + Widget: Into>, +{ + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into> { + self(state, window) + } +} diff --git a/src/app/settings.rs b/src/app/settings.rs index af976d9..338206e 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -20,7 +20,6 @@ pub struct Settings { pub(crate) autosize: bool, /// Set the application to not create a main window - #[cfg(feature = "wayland")] pub(crate) no_main_window: bool, /// Whether the window should have a border, a title bar, etc. or not. @@ -77,7 +76,6 @@ impl Default for Settings { antialiasing: true, #[cfg(feature = "wayland")] autosize: false, - #[cfg(feature = "wayland")] no_main_window: false, client_decorations: true, debug: false, diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 2ee8230..4a6db2c 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -2,7 +2,7 @@ pub mod token; use crate::{ - app::Core, + app::{self, iced_settings, Core}, cctk::sctk, iced::{ self, @@ -10,9 +10,13 @@ use crate::{ widget::Container, window, Color, Length, Limits, Rectangle, }, - iced_style, iced_widget, + iced_widget, theme::{self, system_dark, system_light, Button, THEME}, - widget::{self, layer_container}, + widget::{ + self, + autosize::{autosize, Autosize}, + layer_container, + }, Application, Element, Renderer, }; use cctk::sctk::shell::xdg::window::WindowConfigure; @@ -21,14 +25,16 @@ use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; use cosmic_theme::Theme; use iced::Pixels; use iced_core::{Padding, Shadow}; -use iced_style::container::Appearance; -use iced_widget::runtime::command::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, -}; +use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; -use std::{borrow::Cow, num::NonZeroU32, rc::Rc}; +use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock}; +use tracing::info; use crate::app::cosmic; +static AUTOSIZE_ID: LazyLock = + LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize")); +static AUTOSIZE_MAIN_ID: LazyLock = + LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); #[derive(Debug, Clone)] pub struct Context { @@ -37,9 +43,9 @@ pub struct Context { pub background: CosmicPanelBackground, pub output_name: String, pub panel_type: PanelType, - /// Includes the suggested size of the window. + /// Includes the configured size of the window. /// This can be used by apples to handle overflow themselves. - pub configure: Option, + pub suggested_bounds: Option<(iced::Size)>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -94,7 +100,7 @@ impl Default for Context { .unwrap_or(CosmicPanelBackground::ThemeDefault), output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(), panel_type: PanelType::from(std::env::var("COSMIC_PANEL_NAME").unwrap_or_default()), - configure: None, + suggested_bounds: None, } } } @@ -116,19 +122,21 @@ impl Context { let suggested = self.suggested_size(true); let applet_padding = self.suggested_padding(true); let configured_width = self - .configure + .suggested_bounds .as_ref() - .and_then(|c| c.new_size.0.map(|w| w)) + .and_then(|c| NonZeroU32::new(c.width as u32)) // TODO: should this be physical size instead of logical? .unwrap_or_else(|| { NonZeroU32::new(suggested.0 as u32 + applet_padding as u32 * 2).unwrap() }); + let configured_height = self - .configure + .suggested_bounds .as_ref() - .and_then(|c| c.new_size.1.map(|h| h)) + .and_then(|c| NonZeroU32::new(c.height as u32)) .unwrap_or_else(|| { NonZeroU32::new(suggested.1 as u32 + applet_padding as u32 * 2).unwrap() }); + info!("{configured_height:?}"); (configured_width, configured_height) } @@ -149,18 +157,15 @@ impl Context { #[allow(clippy::cast_precision_loss)] pub fn window_settings(&self) -> crate::app::Settings { let (width, height) = self.suggested_size(true); - let width = f32::from(width); - let height = f32::from(height); let applet_padding = self.suggested_padding(true); + let width = f32::from(width) + applet_padding as f32 * 2.; + let height = f32::from(height) + applet_padding as f32 * 2.; let mut settings = crate::app::Settings::default() - .size(iced_core::Size::new( - width + applet_padding as f32 * 2., - height + applet_padding as f32 * 2., - )) + .size(iced_core::Size::new(width, height)) .size_limits( Limits::NONE - .min_height(height as f32 + applet_padding as f32 * 2.0) - .min_width(width as f32 + applet_padding as f32 * 2.0), + .min_height(height as f32) + .min_width(width as f32), ) .resizable(None) .default_text_size(14.0) @@ -169,6 +174,7 @@ impl Context { if let Some(theme) = self.theme() { settings = settings.theme(theme); } + settings.exit_on_close = true; settings } @@ -182,26 +188,18 @@ impl Context { &self, icon: widget::icon::Handle, ) -> crate::widget::Button<'a, Message> { - let suggested = self.suggested_size(icon.symbolic); + let mut suggested = self.suggested_size(icon.symbolic); let applet_padding = self.suggested_padding(icon.symbolic); - let (mut configured_width, mut configured_height) = self.suggested_window_size(); - // Adjust the width to include padding and force the crosswise dim to match the window size let is_horizontal = self.is_horizontal(); - if is_horizontal { - configured_width = - NonZeroU32::new(suggested.0 as u32 + applet_padding as u32 * 2).unwrap(); - } else { - configured_height = - NonZeroU32::new(suggested.1 as u32 + applet_padding as u32 * 2).unwrap(); - } + let symbolic = icon.symbolic; crate::widget::button::custom( layer_container( widget::icon(icon) - .style(if symbolic { - theme::Svg::Custom(Rc::new(|theme| crate::iced_style::svg::Appearance { + .class(if symbolic { + theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { color: Some(theme.cosmic().background.on.into()), })) } else { @@ -215,9 +213,9 @@ impl Context { .width(Length::Fill) .height(Length::Fill), ) - .width(Length::Fixed(configured_width.get() as f32)) - .height(Length::Fixed(configured_height.get() as f32)) - .style(Button::AppletIcon) + .width(Length::Fixed((suggested.0 + 2 * applet_padding) as f32)) + .height(Length::Fixed((suggested.1 + 2 * applet_padding) as f32)) + .class(Button::AppletIcon) } #[must_use] @@ -225,10 +223,11 @@ impl Context { &self, icon_name: &'a str, ) -> crate::widget::Button<'a, Message> { + let suggested_size = self.suggested_size(true); self.icon_button_from_handle( widget::icon::from_name(icon_name) .symbolic(true) - .size(self.suggested_size(true).0) + .size(suggested_size.0) .into(), ) } @@ -237,7 +236,7 @@ impl Context { pub fn popup_container<'a, Message: 'static>( &self, content: impl Into>, - ) -> Container<'a, Message, crate::Theme, Renderer> { + ) -> Autosize<'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), @@ -245,12 +244,12 @@ impl Context { PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), }; - Container::::new( - Container::::new(content).style(theme::Container::custom( - |theme| { + autosize( + Container::::new( + Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii.clone(); - Appearance { + iced_widget::container::Style { text_color: Some(cosmic.background.on.into()), background: Some(Color::from(cosmic.background.base).into()), border: iced::Border { @@ -261,13 +260,21 @@ impl Context { shadow: Shadow::default(), icon_color: Some(cosmic.background.on.into()), } - }, - )), + }), + ) + .width(Length::Shrink) + .height(Length::Shrink) + .align_x(horizontal_align) + .align_y(vertical_align), + AUTOSIZE_ID.clone(), + ) + .limits( + Limits::NONE + .min_width(1.) + .min_height(1.) + .max_width(500.) + .max_height(1000.), ) - .width(Length::Shrink) - .height(Length::Shrink) - .align_x(horizontal_align) - .align_y(vertical_align) } #[must_use] @@ -282,7 +289,7 @@ impl Context { ) -> SctkPopupSettings { let (width, height) = self.suggested_size(true); let applet_padding = self.suggested_padding(true); - let pixel_offset = 8; + let pixel_offset = 4; let (offset, anchor, gravity) = match self.anchor { PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right), PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left), @@ -312,6 +319,35 @@ impl Context { } } + pub fn autosize_window<'a, Message: 'static>( + &self, + content: impl Into>, + ) -> Autosize<'a, Message, crate::Theme, crate::Renderer> { + let force_configured = matches!(&self.panel_type, &PanelType::Other(ref n) if n.is_empty()); + let w = autosize(content, AUTOSIZE_MAIN_ID.clone()); + let mut limits = Limits::NONE; + let suggested_window_size = self.suggested_window_size(); + + if let Some(width) = self + .suggested_bounds + .as_ref() + .filter(|c| c.width as i32 > 0) + .map(|c| c.width) + { + limits = limits.width(width as f32); + } + if let Some(height) = self + .suggested_bounds + .as_ref() + .filter(|c| c.height as i32 > 0) + .map(|c| c.height) + { + limits = limits.height(height as f32); + } + + w.limits(limits) + } + #[must_use] pub fn theme(&self) -> Option { match self.background { @@ -348,77 +384,67 @@ impl Context { /// # Errors /// /// Returns error on application failure. -pub fn run(autosize: bool, flags: App::Flags) -> iced::Result { +pub fn run(flags: App::Flags) -> iced::Result { let helper = Context::default(); - let mut settings = helper.window_settings(); - settings.autosize = autosize; - if autosize { - settings.size_limits = Limits::NONE; - } - if let Some(icon_theme) = settings.default_icon_theme { + let mut settings = helper.window_settings(); + settings.resizable = None; + + if let Some(icon_theme) = settings.default_icon_theme.clone() { crate::icon_theme::set_default(icon_theme); } - let (width, height) = (settings.size.width as u32, settings.size.height as u32); + THEME + .lock() + .unwrap() + .set_theme(settings.theme.theme_type.clone()); - let mut core = Core::default(); - core.window.show_window_menu = false; + let (iced_settings, (mut core, flags, mut window_settings)) = + iced_settings::(settings, flags); core.window.show_headerbar = false; core.window.sharp_corners = true; core.window.show_maximize = false; core.window.show_minimize = false; core.window.use_template = false; - core.debug = settings.debug; - core.set_scale_factor(settings.scale_factor); - core.set_window_width(width); - core.set_window_height(height); + window_settings.decorations = false; + window_settings.exit_on_close_request = true; + window_settings.resizable = false; + window_settings.resize_border = 0; - THEME.lock().unwrap().set_theme(settings.theme.theme_type); + // TODO make multi-window not mandatory - let mut iced = iced::Settings::with_flags((core, flags)); - - iced.antialiasing = settings.antialiasing; - iced.default_font = settings.default_font; - iced.default_text_size = settings.default_text_size.into(); - iced.id = Some(App::APP_ID.to_owned()); - - { - use iced::wayland::actions::window::SctkWindowSettings; - use iced_sctk::settings::InitialSurface; - iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings { - app_id: Some(App::APP_ID.to_owned()), - autosize: settings.autosize, - client_decorations: settings.client_decorations, - resizable: settings.resizable, - size: (width, height), - size_limits: settings.size_limits, - title: None, - transparent: settings.transparent, - ..SctkWindowSettings::default() - }); + let mut app = super::app::multi_window::multi_window( + cosmic::Cosmic::title, + cosmic::Cosmic::update, + cosmic::Cosmic::view, + ); + if core.main_window.get().is_none() { + app = app.window(window_settings.clone()); + _ = core.main_window.set(iced_core::window::Id::RESERVED); } - - as iced::Application>::run(iced) + app.subscription(cosmic::Cosmic::subscription) + .style(cosmic::Cosmic::style) + .theme(cosmic::Cosmic::theme) + .settings(iced_settings) + .run_with(move || cosmic::Cosmic::::init((core, flags, window_settings))) } #[must_use] -pub fn style() -> ::Style { - ::Style::Custom(Box::new(|theme| { - iced_style::application::Appearance { - background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), - text_color: theme.cosmic().on_bg_color().into(), - icon_color: theme.cosmic().on_bg_color().into(), - } - })) +pub fn style() -> iced_runtime::Appearance { + let theme = crate::theme::THEME.lock().unwrap(); + iced_runtime::Appearance { + background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), + text_color: theme.cosmic().on_bg_color().into(), + icon_color: theme.cosmic().on_bg_color().into(), + } } pub fn menu_button<'a, Message>( content: impl Into>, ) -> crate::widget::Button<'a, Message> { crate::widget::button::custom(content) - .style(Button::AppletMenu) + .class(Button::AppletMenu) .padding(menu_control_padding()) .width(Length::Fill) } diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index c48e125..e3cc619 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,11 +1,12 @@ use crate::iced; -use crate::iced::subscription; use crate::iced_futures::futures; use cctk::sctk::reexports::calloop; use futures::{ channel::mpsc::{unbounded, UnboundedReceiver}, SinkExt, StreamExt, }; +use iced::Subscription; +use iced_futures::stream; use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; use super::wayland_handler::wayland_handler; @@ -13,13 +14,16 @@ use super::wayland_handler::wayland_handler; pub fn activation_token_subscription( id: I, ) -> iced::Subscription { - subscription::channel(id, 50, move |mut output| async move { - let mut state = State::Ready; + Subscription::run_with_id( + id, + stream::channel(50, move |mut output| async move { + let mut state = State::Ready; - loop { - state = start_listening(state, &mut output).await; - } - }) + loop { + state = start_listening(state, &mut output).await; + } + }), + ) } pub enum State { diff --git a/src/command/mod.rs b/src/command/mod.rs index 10e32a2..2a509bc 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -4,114 +4,55 @@ //! Create asynchronous actions to be performed in the background. use iced::window; -use iced::Command; +use iced::Task; use iced_core::window::Mode; -#[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific::wayland::window::Action as WindowAction; -#[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific::wayland::Action as WaylandAction; -#[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific::Action as PlatformAction; -use iced_runtime::command::Action; +use iced_runtime::{task, Action}; use std::future::Future; /// Yields a command which contains a batch of commands. -pub fn batch, Y: 'static>( - commands: impl IntoIterator>, -) -> Command { - Command::batch(commands).map(Into::into) +pub fn batch, Y: Send + 'static>( + commands: impl IntoIterator>, +) -> Task { + Task::batch(commands).map(Into::into) } /// Yields a command which will run the future on the runtime executor. -pub fn future, Y>(future: impl Future + Send + 'static) -> Command { - Command::single(Action::Future(Box::pin(async move { future.await.into() }))) +pub fn future, Y: 'static>(future: impl Future + Send + 'static) -> Task { + Task::future(async move { future.await.into() }) } /// Yields a command which will return a message. -pub fn message, Y>(message: X) -> Command { +pub fn message, Y: 'static>(message: X) -> Task { future(async move { message.into() }) } /// Initiates a window drag. -#[cfg(feature = "wayland")] -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(id: Option) -> Command { - iced_runtime::window::drag(id.unwrap_or(window::Id::MAIN)) +pub fn drag(id: window::Id) -> Task { + iced_runtime::window::drag(id) } /// Maximizes the window. -#[cfg(feature = "wayland")] -pub fn maximize(id: Option, maximized: bool) -> Command { - iced_sctk::commands::window::maximize(id.unwrap_or(window::Id::MAIN), maximized) -} - -/// Maximizes the window. -#[cfg(not(feature = "wayland"))] -pub fn maximize(id: Option, maximized: bool) -> Command { - iced_runtime::window::maximize(id.unwrap_or(window::Id::MAIN), maximized) +pub fn maximize(id: window::Id, maximized: bool) -> Task { + iced_runtime::window::maximize(id, maximized) } /// Minimizes the window. -#[cfg(feature = "wayland")] -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(id: Option) -> Command { - iced_runtime::window::minimize(id.unwrap_or(window::Id::MAIN), true) +pub fn minimize(id: window::Id) -> Task { + iced_runtime::window::minimize(id, true) } /// Sets the title of a window. -#[cfg(feature = "wayland")] -pub fn set_title(id: Option, title: String) -> Command { - window_action(WindowAction::Title { - id: id.unwrap_or(window::Id::MAIN), - title, - }) -} - -/// Sets the title of a window. -#[cfg(not(feature = "wayland"))] #[allow(unused_variables, clippy::needless_pass_by_value)] -pub fn set_title(id: Option, title: String) -> Command { - Command::none() +pub fn set_title(id: window::Id, title: String) -> Task { + Task::none() } /// Sets the window mode to windowed. -#[cfg(feature = "wayland")] -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(id: Option) -> Command { - iced_runtime::window::change_mode(id.unwrap_or(window::Id::MAIN), Mode::Windowed) +pub fn set_windowed(id: window::Id) -> Task { + iced_runtime::window::change_mode(id, Mode::Windowed) } /// Toggles the windows' maximize state. -#[cfg(feature = "wayland")] -pub fn toggle_maximize(id: Option) -> Command { - iced_sctk::commands::window::toggle_maximize(id.unwrap_or(window::Id::MAIN)) -} - -/// Toggles the windows' maximize state. -#[cfg(not(feature = "wayland"))] -pub fn toggle_maximize(id: Option) -> Command { - iced_runtime::window::toggle_maximize(id.unwrap_or(window::Id::MAIN)) -} - -/// Creates a command to apply an action to a window. -#[cfg(feature = "wayland")] -pub fn window_action(action: WindowAction) -> Command { - Command::single(Action::PlatformSpecific(PlatformAction::Wayland( - WaylandAction::Window(action), - ))) +pub fn toggle_maximize(id: window::Id) -> Task { + iced_runtime::window::toggle_maximize(id) } diff --git a/src/ext.rs b/src/ext.rs index beb18c5..215f7b7 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -64,7 +64,7 @@ impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Column<'a where E: Into>, { - self.extend(other.drain(..)) + self.extend(other.drain(..).map(Into::into)) } fn push(self, element: impl Into>) -> Self { @@ -77,7 +77,7 @@ impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, M where E: Into>, { - self.extend(other.drain(..)) + self.extend(other.drain(..).map(Into::into)) } fn push(self, element: impl Into>) -> Self { diff --git a/src/font.rs b/src/font.rs index fae63d2..c390cb6 100644 --- a/src/font.rs +++ b/src/font.rs @@ -4,7 +4,7 @@ //! Select preferred fonts. pub use iced::Font; -use iced_core::font::{Family, Weight}; +use iced_core::font::Weight; pub fn default() -> Font { Font::from(crate::config::interface_font()) diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index ae4c0f9..a45e667 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -17,7 +17,7 @@ pub enum Message { } pub fn subscription() -> Subscription { - listen_raw(|event, status| { + listen_raw(|event, status, _| { if event::Status::Ignored != status { return None; } diff --git a/src/lib.rs b/src/lib.rs index 9d4b6e8..fc8758c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,28 +4,25 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] -#[cfg(all(feature = "wayland", feature = "winit"))] -compile_error!("cannot use `wayland` feature with `winit`"); - /// Recommended default imports. pub mod prelude { pub use crate::ext::*; - #[cfg(any(feature = "winit", feature = "wayland"))] + #[cfg(feature = "winit")] pub use crate::ApplicationExt; pub use crate::{Also, Apply, Element, Renderer, Theme}; } pub use apply::{Also, Apply}; -#[cfg(any(feature = "winit", feature = "wayland"))] +#[cfg(feature = "winit")] pub mod app; -#[cfg(any(feature = "winit", feature = "wayland"))] +#[cfg(feature = "winit")] pub use app::{Application, ApplicationExt}; #[cfg(feature = "applet")] pub mod applet; -pub use iced::Command; +pub use iced::Task; pub mod command; pub mod config; @@ -62,12 +59,6 @@ pub use iced_renderer; #[doc(inline)] pub use iced_runtime; -#[cfg(feature = "wayland")] -pub use iced_sctk; - -#[doc(inline)] -pub use iced_style; - #[doc(inline)] pub use iced_widget; @@ -96,7 +87,7 @@ pub mod theme; pub use theme::{style, Theme}; pub mod widget; - +type Plain = iced_core::text::paragraph::Plain<::Paragraph>; type Paragraph = ::Paragraph; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, crate::Theme, Renderer>; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 292c0dc..30346ec 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -14,6 +14,7 @@ use cosmic_config::CosmicConfigEntry; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; use iced_futures::Subscription; +use iced_runtime::{Appearance, DefaultStyle}; use std::sync::{Arc, Mutex}; @@ -273,3 +274,14 @@ impl LayeredTheme for Theme { self.layer = layer; } } + +impl DefaultStyle for Theme { + fn default_style(&self) -> Appearance { + let cosmic = self.cosmic(); + Appearance { + icon_color: cosmic.bg_color().into(), + background_color: cosmic.bg_color().into(), + text_color: cosmic.on_bg_color().into(), + } + } +} diff --git a/src/theme/portal.rs b/src/theme/portal.rs index b0fc5f8..17225cc 100644 --- a/src/theme/portal.rs +++ b/src/theme/portal.rs @@ -1,7 +1,7 @@ use ashpd::desktop::settings::{ColorScheme, Contrast}; use ashpd::desktop::Color; use iced::futures::{self, select, FutureExt, SinkExt, StreamExt}; -use iced_futures::subscription; +use iced_futures::{stream, subscription}; use tracing::error; #[derive(Debug, Clone)] @@ -12,86 +12,92 @@ pub enum Desktop { } pub fn desktop_settings() -> iced_futures::Subscription { - subscription::channel(std::any::TypeId::of::(), 10, |mut tx| { - async move { - let mut attempts = 0; - loop { - let Ok(settings) = ashpd::desktop::settings::Settings::new().await else { - error!("Failed to create the settings proxy"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))) - .await; - #[cfg(not(feature = "tokio"))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - continue; - }; - - match settings.color_scheme().await { - Ok(color_scheme) => { - let _ = tx.send(Desktop::ColorScheme(color_scheme)).await; - } - Err(err) => error!("Failed to get the color scheme {err:?}"), - }; - match settings.contrast().await { - Ok(contrast) => { - let _ = tx.send(Desktop::Contrast(contrast)).await; - } - Err(err) => error!("Failed to get the contrast {err:?}"), - }; - - let mut color_scheme_stream = settings.receive_color_scheme_changed().await.ok(); - if color_scheme_stream.is_none() { - error!("Failed to receive color scheme changes"); - } - - let mut contrast_stream = settings.receive_contrast_changed().await.ok(); - if contrast_stream.is_none() { - error!("Failed to receive contrast changes"); - } - + iced_futures::Subscription::run_with_id( + std::any::TypeId::of::(), + stream::channel(10, |mut tx| { + async move { + let mut attempts = 0; loop { - if color_scheme_stream.is_none() && contrast_stream.is_none() { - break; + let Ok(settings) = ashpd::desktop::settings::Settings::new().await else { + error!("Failed to create the settings proxy"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(feature = "tokio"))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + continue; + }; + + match settings.color_scheme().await { + Ok(color_scheme) => { + let _ = tx.send(Desktop::ColorScheme(color_scheme)).await; + } + Err(err) => error!("Failed to get the color scheme {err:?}"), + }; + match settings.contrast().await { + Ok(contrast) => { + let _ = tx.send(Desktop::Contrast(contrast)).await; + } + Err(err) => error!("Failed to get the contrast {err:?}"), + }; + + let mut color_scheme_stream = + settings.receive_color_scheme_changed().await.ok(); + if color_scheme_stream.is_none() { + error!("Failed to receive color scheme changes"); } - let next_color_scheme = async { - if let Some(s) = color_scheme_stream.as_mut() { - return s.next().await; - } - futures::future::pending().await - }; - let next_contrast = async { - if let Some(s) = contrast_stream.as_mut() { - return s.next().await; - } - futures::future::pending().await - }; + let mut contrast_stream = settings.receive_contrast_changed().await.ok(); + if contrast_stream.is_none() { + error!("Failed to receive contrast changes"); + } - select! { - s = next_color_scheme.fuse() => { - if let Some(s) = s { - _ = tx.send(Desktop::ColorScheme(s)).await; - } else { - color_scheme_stream = None; + loop { + if color_scheme_stream.is_none() && contrast_stream.is_none() { + break; + } + let next_color_scheme = async { + if let Some(s) = color_scheme_stream.as_mut() { + return s.next().await; } - }, + futures::future::pending().await + }; - c = next_contrast.fuse() => { - if let Some(c) = c { - _ = tx.send(Desktop::Contrast(c)).await; - } else { - contrast_stream = None; + let next_contrast = async { + if let Some(s) = contrast_stream.as_mut() { + return s.next().await; } - } - }; - // Reset the attempts counter if we successfully received a change - attempts = 0; + futures::future::pending().await + }; + + select! { + s = next_color_scheme.fuse() => { + if let Some(s) = s { + _ = tx.send(Desktop::ColorScheme(s)).await; + } else { + color_scheme_stream = None; + } + }, + + c = next_contrast.fuse() => { + if let Some(c) = c { + _ = tx.send(Desktop::Contrast(c)).await; + } else { + contrast_stream = None; + } + } + }; + // Reset the attempts counter if we successfully received a change + attempts = 0; + } } } - } - }) + }), + ) } diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index c2dc655..873a390 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -8,17 +8,17 @@ use iced_core::{Background, Color}; use crate::{ theme::TRANSPARENT_COMPONENT, - widget::button::{Appearance, StyleSheet}, + widget::button::{Catalog, Style}, }; #[derive(Default)] pub enum Button { AppletIcon, Custom { - active: Box Appearance>, - disabled: Box Appearance>, - hovered: Box Appearance>, - pressed: Box Appearance>, + active: Box Style>, + disabled: Box Style>, + hovered: Box Style>, + pressed: Box Style>, }, AppletMenu, Destructive, @@ -44,10 +44,10 @@ pub fn appearance( disabled: bool, style: &Button, color: impl Fn(&Component) -> (Color, Option, Option), -) -> Appearance { +) -> Style { let cosmic = theme.cosmic(); let mut corner_radii = &cosmic.corner_radii.radius_xl; - let mut appearance = Appearance::new(); + let mut appearance = Style::new(); match style { Button::Standard @@ -163,10 +163,10 @@ pub fn appearance( appearance } -impl StyleSheet for crate::Theme { - type Style = Button; +impl Catalog for crate::Theme { + type Class = Button; - fn active(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance { + fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { if let Button::Custom { active, .. } = style { return active(focused, self); } @@ -186,7 +186,7 @@ impl StyleSheet for crate::Theme { }) } - fn disabled(&self, style: &Self::Style) -> Appearance { + fn disabled(&self, style: &Self::Class) -> Style { if let Button::Custom { disabled, .. } = style { return disabled(self); } @@ -202,11 +202,11 @@ impl StyleSheet for crate::Theme { }) } - fn drop_target(&self, style: &Self::Style) -> Appearance { + fn drop_target(&self, style: &Self::Class) -> Style { self.active(false, false, style) } - fn hovered(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance { + fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { if let Button::Custom { hovered, .. } = style { return hovered(focused, self); } @@ -233,7 +233,7 @@ impl StyleSheet for crate::Theme { ) } - fn pressed(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance { + fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { if let Button::Custom { pressed, .. } = style { return pressed(focused, self); } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 9b4095e..73be693 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -5,52 +5,44 @@ use crate::theme::{CosmicComponent, Theme, TRANSPARENT_COMPONENT}; use cosmic_theme::composite::over; +use iced::{ + overlay::menu, + widget::{ + button as iced_button, checkbox as iced_checkbox, container as iced_container, pane_grid, + pick_list, progress_bar, radio, rule, scrollable, + slider::{self, Rail}, + svg, toggler, + }, + Gradient, +}; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_style::application; -use iced_style::button as iced_button; -use iced_style::checkbox; -use iced_style::container; -use iced_style::menu; -use iced_style::pane_grid; -use iced_style::pick_list; -use iced_style::progress_bar; -use iced_style::radio; -use iced_style::rule; -use iced_style::scrollable; -use iced_style::slider; -use iced_style::slider::Rail; -use iced_style::svg; -use iced_style::text_input; -use iced_style::toggler; - +use iced_widget::{pane_grid::Highlight, text_editor, text_input}; use std::rc::Rc; -#[derive(Default)] -pub enum Application { - #[default] - Default, - Custom(Box application::Appearance>), -} +pub mod application { + use crate::Theme; + use iced_runtime::Appearance; -impl Application { - pub fn custom application::Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) + #[derive(Default)] + pub enum Application { + #[default] + Default, + Custom(Box Appearance>), } -} -impl application::StyleSheet for Theme { - type Style = Application; + impl Application { + pub fn custom Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } + } - fn appearance(&self, style: &Self::Style) -> application::Appearance { - let cosmic = self.cosmic(); + pub fn appearance(theme: &Theme) -> Appearance { + let cosmic = theme.cosmic(); - match style { - Application::Default => application::Appearance { - icon_color: cosmic.bg_color().into(), - background_color: cosmic.bg_color().into(), - text_color: cosmic.on_bg_color().into(), - }, - Application::Custom(f) => f(self), + Appearance { + icon_color: cosmic.bg_color().into(), + background_color: cosmic.bg_color().into(), + text_color: cosmic.on_bg_color().into(), } } } @@ -69,10 +61,84 @@ pub enum Button { LinkActive, Transparent, Card, - Custom { - active: Box iced_button::Appearance>, - hover: Box iced_button::Appearance>, - }, + Custom(Box iced_button::Style>), +} + +impl iced_button::Catalog for Theme { + type Class<'a> = Button; + + fn default<'a>() -> Self::Class<'a> { + Button::default() + } + + fn style(&self, class: &Self::Class<'_>, status: iced_button::Status) -> iced_button::Style { + if let Button::Custom(f) = class { + return f(self, status); + } + let cosmic = self.cosmic(); + let corner_radii = &cosmic.corner_radii; + let component = class.cosmic(self); + + let mut appearance = iced_button::Style { + border_radius: match class { + Button::Link => corner_radii.radius_0.into(), + Button::Card => corner_radii.radius_xs.into(), + _ => corner_radii.radius_xl.into(), + }, + border: Border { + radius: match class { + Button::Link => corner_radii.radius_0.into(), + Button::Card => corner_radii.radius_xs.into(), + _ => corner_radii.radius_xl.into(), + }, + ..Default::default() + }, + background: match class { + Button::Link | Button::Text => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.base.into())), + }, + text_color: match class { + Button::Link | Button::LinkActive => component.base.into(), + _ => component.on.into(), + }, + ..iced_button::Style::default() + }; + + match status { + iced_button::Status::Active => {} + iced_button::Status::Hovered => { + appearance.background = match class { + Button::Link => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.hover.into())), + }; + } + iced_button::Status::Pressed => { + appearance.background = match class { + Button::Link => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.pressed.into())), + }; + } + iced_button::Status::Disabled => { + appearance.background = appearance.background.map(|background| match background { + Background::Color(color) => Background::Color(Color { + a: color.a * 0.5, + ..color + }), + Background::Gradient(gradient) => { + Background::Gradient(gradient.scale_alpha(0.5)) + } + }); + appearance.text_color = Color { + a: appearance.text_color.a * 0.5, + ..appearance.text_color + }; + } + }; + appearance + } } impl Button { @@ -96,86 +162,6 @@ impl Button { } } -impl iced_button::StyleSheet for Theme { - type Style = Button; - - fn active(&self, style: &Self::Style) -> iced_button::Appearance { - if let Button::Custom { active, .. } = style { - return active(self); - } - - let corner_radii = &self.cosmic().corner_radii; - let component = style.cosmic(self); - iced_button::Appearance { - border_radius: match style { - Button::Link => corner_radii.radius_0.into(), - Button::Card => corner_radii.radius_xs.into(), - _ => corner_radii.radius_xl.into(), - }, - border: Border { - radius: match style { - Button::Link => corner_radii.radius_0.into(), - Button::Card => corner_radii.radius_xs.into(), - _ => corner_radii.radius_xl.into(), - }, - ..Default::default() - }, - background: match style { - Button::Link | Button::Text => None, - Button::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.base.into())), - }, - text_color: match style { - Button::Link | Button::LinkActive => component.base.into(), - _ => component.on.into(), - }, - ..iced_button::Appearance::default() - } - } - - fn hovered(&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); - - if matches!(style, Button::Card) { - return active; - } - - iced_button::Appearance { - shadow_offset: Vector::default(), - background: active.background.map(|background| match background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => Background::Gradient(gradient.mul_alpha(0.5)), - }), - text_color: Color { - a: active.text_color.a * 0.5, - ..active.text_color - }, - ..active - } - } -} - /* * TODO: Checkbox */ @@ -193,169 +179,193 @@ impl Default for Checkbox { } } -impl checkbox::StyleSheet for Theme { - type Style = Checkbox; +impl iced_checkbox::Catalog for Theme { + type Class<'a> = Checkbox; - fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { - let cosmic = self.cosmic(); - - let corners = &cosmic.corner_radii; - match style { - Checkbox::Primary => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.accent.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.accent.on.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 { - background: Background::Color(if is_checked { - cosmic.background.component.base.into() - } else { - cosmic.background.base.into() - }), - icon_color: cosmic.background.on.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 { - background: Background::Color(if is_checked { - cosmic.success.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.success.on.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 { - background: Background::Color(if is_checked { - cosmic.destructive.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.destructive.on.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, - }, - } + fn default<'a>() -> Self::Class<'a> { + Checkbox::default() } - fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { + fn style( + &self, + class: &Self::Class<'_>, + status: iced_checkbox::Status, + ) -> iced_checkbox::Style { let cosmic = self.cosmic(); + let corners = &cosmic.corner_radii; - match style { - Checkbox::Primary => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.accent.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.accent.on.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 + let disabled = matches!(status, iced_checkbox::Status::Disabled { .. }); + match status { + iced_checkbox::Status::Active { is_checked } + | iced_checkbox::Status::Disabled { is_checked } => { + let mut active = match class { + Checkbox::Primary => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.accent.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.accent.on.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 => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.background.component.base.into() + } else { + cosmic.background.base.into() + }), + icon_color: cosmic.background.on.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 => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.success.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.success.on.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 => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.destructive.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.destructive.on.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, + }, + }; + if disabled { + match &mut active.background { + Background::Color(color) => { + color.a /= 2.; + } + Background::Gradient(gradient) => { + *gradient = gradient.scale_alpha(0.5); + } } - .into(), - }, - text_color: None, - }, - Checkbox::Secondary => checkbox::Appearance { - background: Background::Color(if is_checked { - self.current_container().base.into() - } else { - cosmic.button.base.into() - }), - icon_color: self.current_container().on.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 + if let Some(c) = active.text_color.as_mut() { + c.a /= 2. + }; + active.border.color.a /= 2.; + } + active + } + iced_checkbox::Status::Hovered { is_checked } => match class { + Checkbox::Primary => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.accent.base.into() } else { - cosmic.button.border - } - .into(), + cosmic.button.base.into() + }), + icon_color: cosmic.accent.on.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, }, - text_color: None, - }, - Checkbox::Success => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.success.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.success.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.success.base + Checkbox::Secondary => iced_checkbox::Style { + background: Background::Color(if is_checked { + self.current_container().base.into() } else { - cosmic.button.border - } - .into(), + cosmic.button.base.into() + }), + icon_color: self.current_container().on.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, }, - text_color: None, - }, - Checkbox::Danger => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.destructive.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.destructive.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.destructive.base + Checkbox::Success => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.success.base.into() } else { - cosmic.button.border - } - .into(), + cosmic.button.base.into() + }), + icon_color: cosmic.success.on.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 => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.destructive.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.destructive.on.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, }, - text_color: None, }, } } @@ -365,12 +375,12 @@ impl checkbox::StyleSheet for Theme { * TODO: Container */ #[derive(Default)] -pub enum Container { +pub enum Container<'a> { WindowBackground, Background, Card, ContextDrawer, - Custom(Box container::Appearance>), + Custom(Box iced_container::Style + 'a>), Dialog, Dropdown, HeaderBar { @@ -384,14 +394,14 @@ pub enum Container { Transparent, } -impl Container { - pub fn custom container::Appearance + 'static>(f: F) -> Self { +impl<'a> Container<'a> { + pub fn custom iced_container::Style + 'a>(f: F) -> Self { Self::Custom(Box::new(f)) } #[must_use] - pub fn background(theme: &cosmic_theme::Theme) -> container::Appearance { - container::Appearance { + pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { + iced_container::Style { icon_color: Some(Color::from(theme.background.on)), text_color: Some(Color::from(theme.background.on)), background: Some(iced::Background::Color(theme.background.base.into())), @@ -404,8 +414,8 @@ impl Container { } #[must_use] - pub fn primary(theme: &cosmic_theme::Theme) -> container::Appearance { - container::Appearance { + pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { + iced_container::Style { icon_color: Some(Color::from(theme.primary.on)), text_color: Some(Color::from(theme.primary.on)), background: Some(iced::Background::Color(theme.primary.base.into())), @@ -418,8 +428,8 @@ impl Container { } #[must_use] - pub fn secondary(theme: &cosmic_theme::Theme) -> container::Appearance { - container::Appearance { + pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { + iced_container::Style { icon_color: Some(Color::from(theme.secondary.on)), text_color: Some(Color::from(theme.secondary.on)), background: Some(iced::Background::Color(theme.secondary.base.into())), @@ -432,19 +442,28 @@ impl Container { } } -impl container::StyleSheet for Theme { - type Style = Container; +impl<'a> From> for Container<'a> { + fn from(value: iced_container::StyleFn<'a, Theme>) -> Self { + Self::custom(value) + } +} - #[allow(clippy::too_many_lines)] - fn appearance(&self, style: &Self::Style) -> container::Appearance { +impl iced_container::Catalog for Theme { + type Class<'a> = Container<'a>; + + fn default<'a>() -> Self::Class<'a> { + Container::default() + } + + fn style(&self, class: &Self::Class<'_>) -> iced_container::Style { let cosmic = self.cosmic(); - match style { - Container::Transparent => container::Appearance::default(), + match class { + Container::Transparent => iced_container::Style::default(), Container::Custom(f) => f(self), - Container::WindowBackground => container::Appearance { + Container::WindowBackground => iced_container::Style { 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())), @@ -463,7 +482,7 @@ impl container::StyleSheet for Theme { Container::List => { let component = &self.current_container().component; - container::Appearance { + iced_container::Style { icon_color: Some(component.on.into()), text_color: Some(component.on.into()), background: Some(Background::Color(component.base.into())), @@ -488,7 +507,7 @@ impl container::StyleSheet for Theme { (unfocused_color, unfocused_color) }; - container::Appearance { + iced_container::Style { icon_color: Some(icon_color), text_color: Some(text_color), background: Some(iced::Background::Color(cosmic.background.base.into())), @@ -533,7 +552,7 @@ impl container::StyleSheet for Theme { Container::Dropdown => { let theme = self.cosmic(); - container::Appearance { + iced_container::Style { icon_color: None, text_color: None, background: Some(iced::Background::Color(theme.primary.base.into())), @@ -545,7 +564,7 @@ impl container::StyleSheet for Theme { } } - Container::Tooltip => container::Appearance { + Container::Tooltip => iced_container::Style { icon_color: None, text_color: None, background: Some(iced::Background::Color(cosmic.palette.neutral_2.into())), @@ -560,7 +579,7 @@ impl container::StyleSheet for Theme { let cosmic = self.cosmic(); match self.layer { - cosmic_theme::Layer::Background => container::Appearance { + cosmic_theme::Layer::Background => iced_container::Style { icon_color: Some(Color::from(cosmic.background.component.on)), text_color: Some(Color::from(cosmic.background.component.on)), background: Some(iced::Background::Color( @@ -572,7 +591,7 @@ impl container::StyleSheet for Theme { }, shadow: Shadow::default(), }, - cosmic_theme::Layer::Primary => container::Appearance { + cosmic_theme::Layer::Primary => iced_container::Style { icon_color: Some(Color::from(cosmic.primary.component.on)), text_color: Some(Color::from(cosmic.primary.component.on)), background: Some(iced::Background::Color( @@ -584,7 +603,7 @@ impl container::StyleSheet for Theme { }, shadow: Shadow::default(), }, - cosmic_theme::Layer::Secondary => container::Appearance { + cosmic_theme::Layer::Secondary => iced_container::Style { icon_color: Some(Color::from(cosmic.secondary.component.on)), text_color: Some(Color::from(cosmic.secondary.component.on)), background: Some(iced::Background::Color( @@ -599,7 +618,7 @@ impl container::StyleSheet for Theme { } } - Container::Dialog => container::Appearance { + Container::Dialog => iced_container::Style { 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())), @@ -623,33 +642,39 @@ pub enum Slider { #[default] Standard, Custom { - active: Rc slider::Appearance>, - hovered: Rc slider::Appearance>, - dragging: Rc slider::Appearance>, + active: Rc slider::Style>, + hovered: Rc slider::Style>, + dragging: Rc slider::Style>, }, } /* * Slider */ -impl slider::StyleSheet for Theme { - type Style = Slider; +impl slider::Catalog for Theme { + type Class<'a> = Slider; - fn active(&self, style: &Self::Style) -> slider::Appearance { - match style { + fn default<'a>() -> Self::Class<'a> { + Slider::default() + } + + fn style(&self, class: &Self::Class<'_>, status: slider::Status) -> slider::Style { + let cosmic: &cosmic_theme::Theme = self.cosmic(); + let mut appearance = match class { Slider::Standard => //TODO: no way to set rail thickness { - let cosmic: &cosmic_theme::Theme = self.cosmic(); - - slider::Appearance { + slider::Style { rail: Rail { - colors: slider::RailBackground::Pair( - cosmic.accent.base.into(), - cosmic.palette.neutral_6.into(), + backgrounds: ( + Background::Color(cosmic.accent.base.into()), + Background::Color(cosmic.palette.neutral_6.into()), ), + border: Border { + radius: cosmic.corner_radii.radius_xs.into(), + ..Border::default() + }, width: 4.0, - border_radius: cosmic.corner_radii.radius_xs.into(), }, handle: slider::Handle { @@ -658,9 +683,9 @@ impl slider::StyleSheet for Theme { width: 20, border_radius: cosmic.corner_radii.radius_m.into(), }, - color: cosmic.accent.base.into(), border_color: Color::TRANSPARENT, border_width: 0.0, + background: Background::Color(cosmic.accent.base.into()), }, breakpoint: slider::Breakpoint { @@ -669,55 +694,61 @@ impl slider::StyleSheet for Theme { } } Slider::Custom { active, .. } => active(self), - } - } + }; + match status { + slider::Status::Active => appearance, + slider::Status::Hovered => match class { + Slider::Standard => { + appearance.handle.shape = slider::HandleShape::Rectangle { + height: 26, + width: 26, + border_radius: cosmic.corner_radii.radius_m.into(), + }; + appearance.handle.border_width = 3.0; + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.1; + appearance.handle.border_color = border_color.into(); + appearance + } + Slider::Custom { hovered, .. } => hovered(self), + }, + slider::Status::Dragged => match class { + Slider::Standard => { + let mut style = { + appearance.handle.shape = slider::HandleShape::Rectangle { + height: 26, + width: 26, + border_radius: cosmic.corner_radii.radius_m.into(), + }; + appearance.handle.border_width = 3.0; + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.1; + appearance.handle.border_color = border_color.into(); + appearance + }; + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.2; + style.handle.border_color = border_color.into(); - fn hovered(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Standard => { - let cosmic: &cosmic_theme::Theme = self.cosmic(); - - let mut style = self.active(style); - style.handle.shape = slider::HandleShape::Rectangle { - height: 26, - width: 26, - border_radius: cosmic.corner_radii.radius_m.into(), - }; - style.handle.border_width = 3.0; - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.1; - style.handle.border_color = border_color.into(); - style - } - Slider::Custom { hovered, .. } => hovered(self), - } - } - - fn dragging(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Standard => { - let mut style = self.hovered(style); - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.2; - style.handle.border_color = border_color.into(); - - style - } - Slider::Custom { dragging, .. } => dragging(self), + style + } + Slider::Custom { dragging, .. } => dragging(self), + }, } } } -/* - * TODO: Menu - */ -impl menu::StyleSheet for Theme { - type Style = (); +impl menu::Catalog for Theme { + type Class<'a> = (); - fn appearance(&self, _style: &Self::Style) -> menu::Appearance { + fn default<'a>() -> ::Class<'a> { + () + } + + fn style(&self, class: &::Class<'_>) -> menu::Style { let cosmic = self.cosmic(); - menu::Appearance { + menu::Style { text_color: cosmic.on_bg_color().into(), background: Background::Color(cosmic.background.base.into()), border: Border { @@ -733,13 +764,21 @@ impl menu::StyleSheet for Theme { /* * TODO: Pick List */ -impl pick_list::StyleSheet for Theme { - type Style = (); +impl pick_list::Catalog for Theme { + type Class<'a> = (); - fn active(&self, _style: &()) -> pick_list::Appearance { + fn default<'a>() -> ::Class<'a> { + () + } + + fn style( + &self, + class: &::Class<'_>, + status: pick_list::Status, + ) -> pick_list::Style { let cosmic = &self.cosmic(); - pick_list::Appearance { + let appearance = pick_list::Style { text_color: cosmic.on_bg_color().into(), background: Color::TRANSPARENT.into(), placeholder_color: cosmic.on_bg_color().into(), @@ -749,15 +788,14 @@ impl pick_list::StyleSheet for Theme { }, // icon_size: 0.7, // TODO: how to replace handle_color: cosmic.on_bg_color().into(), - } - } - - fn hovered(&self, style: &()) -> pick_list::Appearance { - let cosmic = &self.cosmic(); - - pick_list::Appearance { - background: Background::Color(cosmic.background.base.into()), - ..self.active(style) + }; + match status { + pick_list::Status::Active => appearance, + pick_list::Status::Hovered => pick_list::Style { + background: Background::Color(cosmic.background.base.into()), + ..appearance + }, + pick_list::Status::Opened => appearance, } } } @@ -765,52 +803,53 @@ impl pick_list::StyleSheet for Theme { /* * TODO: Radio */ -impl radio::StyleSheet for Theme { - type Style = (); +impl radio::Catalog for Theme { + type Class<'a> = (); - fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { - let theme = self.cosmic(); - - radio::Appearance { - background: if is_selected { - Color::from(theme.accent.base).into() - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.background.base).into() - }, - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_7) - }, - text_color: None, - } + fn default<'a>() -> Self::Class<'a> { + () } - fn hovered(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { + fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let theme = self.cosmic(); let mut neutral_10 = theme.palette.neutral_10; neutral_10.alpha = 0.1; - radio::Appearance { - background: if is_selected { - Color::from(theme.accent.base).into() - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(neutral_10).into() + match status { + radio::Status::Active { is_selected } => radio::Style { + background: if is_selected { + Color::from(theme.accent.base).into() + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.background.base).into() + }, + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.palette.neutral_7) + }, + text_color: None, }, - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_7) + radio::Status::Hovered { is_selected } => radio::Style { + background: if is_selected { + Color::from(theme.accent.base).into() + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(neutral_10).into() + }, + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.palette.neutral_7) + }, + text_color: None, }, - text_color: None, } } } @@ -818,44 +857,56 @@ impl radio::StyleSheet for Theme { /* * Toggler */ -impl toggler::StyleSheet for Theme { - type Style = (); +impl toggler::Catalog for Theme { + type Class<'a> = (); - fn active(&self, _style: &Self::Style, is_active: bool) -> toggler::Appearance { - let theme = self.cosmic(); + fn default<'a>() -> Self::Class<'a> { + () + } + + fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { + let cosmic = self.cosmic(); const HANDLE_MARGIN: f32 = 2.0; - toggler::Appearance { - background: if is_active { - theme.accent.base.into() + let mut neutral_10 = cosmic.palette.neutral_10; + neutral_10.alpha = 0.1; + + let mut active = toggler::Style { + background: if matches!(status, toggler::Status::Active { is_toggled: true }) { + cosmic.accent.base.into() } else { - theme.palette.neutral_5.into() + cosmic.palette.neutral_5.into() }, - background_border: None, - foreground: theme.palette.neutral_2.into(), - foreground_border: None, - border_radius: theme.radius_xl().into(), - handle_radius: theme + foreground: cosmic.palette.neutral_2.into(), + border_radius: cosmic.radius_xl().into(), + handle_radius: cosmic .radius_xl() .map(|x| (x - HANDLE_MARGIN).max(0.0)) .into(), handle_margin: HANDLE_MARGIN, - } - } - - fn hovered(&self, style: &Self::Style, is_active: bool) -> toggler::Appearance { - let cosmic = self.cosmic(); - //TODO: grab colors from palette - let mut neutral_10 = cosmic.palette.neutral_10; - neutral_10.alpha = 0.1; - - toggler::Appearance { - background: if is_active { - over(neutral_10, cosmic.accent_color()) - } else { - over(neutral_10, cosmic.palette.neutral_5) + background_border_width: 0.0, + background_border_color: Color::TRANSPARENT, + foreground_border_width: 0.0, + foreground_border_color: Color::TRANSPARENT, + }; + match status { + toggler::Status::Active { is_toggled } => active, + toggler::Status::Hovered { is_toggled } => { + let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true }); + toggler::Style { + background: if is_active { + over(neutral_10, cosmic.accent_color()) + } else { + over(neutral_10, cosmic.palette.neutral_5) + } + .into(), + ..active + } + } + toggler::Status::Disabled => { + active.background.a /= 2.; + active.foreground.a /= 2.; + active } - .into(), - ..self.active(style, is_active) } } } @@ -863,35 +914,32 @@ impl toggler::StyleSheet for Theme { /* * TODO: Pane Grid */ -impl pane_grid::StyleSheet for Theme { - type Style = (); +impl pane_grid::Catalog for Theme { + type Class<'a> = (); - fn picked_split(&self, _style: &Self::Style) -> Option { - let theme = self.cosmic(); - - Some(pane_grid::Line { - color: theme.accent.base.into(), - width: 2.0, - }) + fn default<'a>() -> ::Class<'a> { + () } - fn hovered_split(&self, _style: &Self::Style) -> Option { + fn style(&self, class: &::Class<'_>) -> pane_grid::Style { let theme = self.cosmic(); - Some(pane_grid::Line { - color: theme.accent.hover.into(), - width: 2.0, - }) - } - - fn hovered_region(&self, _style: &Self::Style) -> pane_grid::Appearance { - let theme = self.cosmic(); - pane_grid::Appearance { - background: Background::Color(theme.bg_color().into()), - border: Border { - radius: theme.corner_radii.radius_0.into(), + pane_grid::Style { + hovered_region: Highlight { + background: Background::Color(theme.bg_color().into()), + border: Border { + radius: theme.corner_radii.radius_0.into(), + width: 2.0, + color: theme.bg_divider().into(), + }, + }, + picked_split: pane_grid::Line { + color: theme.accent.base.into(), + width: 2.0, + }, + hovered_split: pane_grid::Line { + color: theme.accent.hover.into(), width: 2.0, - color: theme.bg_divider().into(), }, } } @@ -906,36 +954,52 @@ pub enum ProgressBar { Primary, Success, Danger, - Custom(Box progress_bar::Appearance>), + Custom(Box progress_bar::Style>), } impl ProgressBar { - pub fn custom progress_bar::Appearance + 'static>(f: F) -> Self { + pub fn custom progress_bar::Style + 'static>(f: F) -> Self { Self::Custom(Box::new(f)) } } -impl progress_bar::StyleSheet for Theme { - type Style = ProgressBar; +impl progress_bar::Catalog for Theme { + type Class<'a> = ProgressBar; - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { + fn default<'a>() -> Self::Class<'a> { + ProgressBar::default() + } + + fn style(&self, class: &Self::Class<'_>) -> progress_bar::Style { let theme = self.cosmic(); - match style { - ProgressBar::Primary => progress_bar::Appearance { + match class { + ProgressBar::Primary => progress_bar::Style { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.accent.base).into(), - border_radius: theme.corner_radii.radius_xs.into(), + border: Border { + color: Color::default(), + width: 0.0, + radius: theme.corner_radii.radius_xs.into(), + }, }, - ProgressBar::Success => progress_bar::Appearance { + ProgressBar::Success => progress_bar::Style { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.success.base).into(), - border_radius: theme.corner_radii.radius_xs.into(), + border: Border { + color: Color::default(), + width: 0.0, + radius: theme.corner_radii.radius_xs.into(), + }, }, - ProgressBar::Danger => progress_bar::Appearance { + ProgressBar::Danger => progress_bar::Style { background: Color::from(theme.background.divider).into(), bar: Color::from(theme.destructive.base).into(), - border_radius: theme.corner_radii.radius_xs.into(), + border: Border { + color: Color::default(), + width: 0.0, + radius: theme.corner_radii.radius_xs.into(), + }, }, ProgressBar::Custom(f) => f(self), } @@ -951,33 +1015,37 @@ pub enum Rule { Default, LightDivider, HeavyDivider, - Custom(Box rule::Appearance>), + Custom(Box rule::Style>), } impl Rule { - pub fn custom rule::Appearance + 'static>(f: F) -> Self { + pub fn custom rule::Style + 'static>(f: F) -> Self { Self::Custom(Box::new(f)) } } -impl rule::StyleSheet for Theme { - type Style = Rule; +impl rule::Catalog for Theme { + type Class<'a> = Rule; - fn appearance(&self, style: &Self::Style) -> rule::Appearance { - match style { - Rule::Default => rule::Appearance { + fn default<'a>() -> Self::Class<'a> { + Rule::default() + } + + fn style(&self, class: &Self::Class<'_>) -> rule::Style { + match class { + Rule::Default => rule::Style { color: self.current_container().divider.into(), width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, }, - Rule::LightDivider => rule::Appearance { + Rule::LightDivider => rule::Style { color: self.current_container().divider.into(), width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Padded(8), }, - Rule::HeavyDivider => rule::Appearance { + Rule::HeavyDivider => rule::Style { color: self.current_container().divider.into(), width: 4, radius: 2.0.into(), @@ -998,99 +1066,148 @@ pub enum Scrollable { /* * TODO: Scrollable */ -impl scrollable::StyleSheet for Theme { - type Style = Scrollable; +impl scrollable::Catalog for Theme { + type Class<'a> = Scrollable; - fn active(&self, style: &Self::Style) -> scrollable::Scrollbar { - let cosmic = self.cosmic(); - let mut neutral_5 = cosmic.palette.neutral_5; - neutral_5.alpha = 0.7; - let mut a = scrollable::Scrollbar { - background: None, - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - scroller: scrollable::Scroller { - color: neutral_5.into(), - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - }, - }; - - 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 default<'a>() -> Self::Class<'a> { + Scrollable::default() } - 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; + fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { + match status { + scrollable::Status::Active => { + let cosmic = self.cosmic(); + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.7; + let mut a = scrollable::Style { + container: iced_container::transparent(self), + vertical_rail: scrollable::Rail { + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + background: None, + scroller: scrollable::Scroller { + color: neutral_5.into(), + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + }, + }, + horizontal_rail: scrollable::Rail { + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + background: None, + scroller: scrollable::Scroller { + color: neutral_5.into(), + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + }, + }, + gap: None, + }; - 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: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - scroller: scrollable::Scroller { - color: neutral_5.into(), - border: Border { - radius: cosmic.corner_radii.radius_s.into(), - ..Default::default() - }, - }, - }; - 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())); - } + if matches!(class, Scrollable::Permanent) { + let mut neutral_3 = cosmic.palette.neutral_3; + neutral_3.alpha = 0.7; + a.horizontal_rail.background = Some(Background::Color(neutral_3.into())); + a.vertical_rail.background = Some(Background::Color(neutral_3.into())); + } - a + a + } + // TODO handle vertical / horizontal + scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => { + let cosmic = self.cosmic(); + let mut neutral_5 = cosmic.palette.neutral_5; + neutral_5.alpha = 0.7; + + // TODO hover + + // 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::Style = scrollable::Style { + container: iced_container::Style::default(), + vertical_rail: scrollable::Rail { + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + background: None, + scroller: scrollable::Scroller { + color: neutral_5.into(), + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + }, + }, + horizontal_rail: scrollable::Rail { + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + background: None, + scroller: scrollable::Scroller { + color: neutral_5.into(), + border: Border { + radius: cosmic.corner_radii.radius_s.into(), + ..Default::default() + }, + }, + }, + gap: None, + }; + + if matches!(class, Scrollable::Permanent) { + let mut neutral_3 = cosmic.palette.neutral_3; + neutral_3.alpha = 0.7; + a.horizontal_rail.background = Some(Background::Color(neutral_3.into())); + a.vertical_rail.background = Some(Background::Color(neutral_3.into())); + } + + a + } + } } } #[derive(Clone, Default)] pub enum Svg { /// Apply a custom appearance filter - Custom(Rc svg::Appearance>), + Custom(Rc svg::Style>), /// No filtering is applied #[default] Default, } impl Svg { - pub fn custom svg::Appearance + 'static>(f: F) -> Self { + pub fn custom svg::Style + 'static>(f: F) -> Self { Self::Custom(Rc::new(f)) } } -impl svg::StyleSheet for Theme { - type Style = Svg; +impl svg::Catalog for Theme { + type Class<'a> = Svg; - fn appearance(&self, style: &Self::Style) -> svg::Appearance { - #[allow(clippy::match_same_arms)] - match style { - Svg::Default => svg::Appearance::default(), - Svg::Custom(appearance) => appearance(self), - } + fn default<'a>() -> Self::Class<'a> { + Svg::default() } - fn hovered(&self, style: &Self::Style) -> svg::Appearance { - self.appearance(style) + fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style { + #[allow(clippy::match_same_arms)] + match class { + Svg::Default => svg::Style::default(), + Svg::Custom(appearance) => appearance(self), + } } } @@ -1104,7 +1221,7 @@ pub enum Text { Default, Color(Color), // TODO: Can't use dyn Fn since this must be copy - Custom(fn(&Theme) -> iced_widget::text::Appearance), + Custom(fn(&Theme) -> iced_widget::text::Style), } impl From for Text { @@ -1113,16 +1230,20 @@ impl From for Text { } } -impl iced_widget::text::StyleSheet for Theme { - type Style = Text; +impl iced_widget::text::Catalog for Theme { + type Class<'a> = Text; - fn appearance(&self, style: Self::Style) -> iced_widget::text::Appearance { - match style { - Text::Accent => iced_widget::text::Appearance { + fn default<'a>() -> Self::Class<'a> { + Text::default() + } + + fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { + match class { + Text::Accent => iced_widget::text::Style { color: Some(self.cosmic().accent.base.into()), }, - Text::Default => iced_widget::text::Appearance { color: None }, - Text::Color(c) => iced_widget::text::Appearance { color: Some(c) }, + Text::Default => iced_widget::text::Style { color: None }, + Text::Color(c) => iced_widget::text::Style { color: Some(*c) }, Text::Custom(f) => f(self), } } @@ -1138,221 +1259,213 @@ pub enum TextInput { /* * TODO: Text Input */ -impl text_input::StyleSheet for Theme { - type Style = TextInput; +impl text_input::Catalog for Theme { + type Class<'a> = TextInput; - fn active(&self, style: &Self::Style) -> text_input::Appearance { + fn default<'a>() -> Self::Class<'a> { + TextInput::default() + } + + fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style { let palette = self.cosmic(); let mut bg = palette.palette.neutral_7; bg.alpha = 0.25; - match style { - TextInput::Default => text_input::Appearance { + + let mut neutral_9 = palette.palette.neutral_9; + let value = neutral_9.into(); + neutral_9.alpha = 0.7; + let placeholder = neutral_9.into(); + let selection = palette.accent.base.into(); + + let mut appearance = match class { + TextInput::Default => text_input::Style { background: Color::from(bg).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(), + icon: self.current_container().on.into(), + placeholder, + value, + selection, }, - TextInput::Search => text_input::Appearance { + TextInput::Search => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_m.into(), ..Default::default() }, - icon_color: self.current_container().on.into(), + icon: self.current_container().on.into(), + placeholder, + value, + selection, }, + }; + + match status { + text_input::Status::Active => appearance, + text_input::Status::Hovered => { + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + + match class { + TextInput::Default => text_input::Style { + background: Color::from(bg).into(), + border: Border { + radius: palette.corner_radii.radius_s.into(), + width: 1.0, + color: self.current_container().on.into(), + }, + icon: self.current_container().on.into(), + placeholder, + value, + selection, + }, + TextInput::Search => text_input::Style { + background: Color::from(bg).into(), + border: Border { + radius: palette.corner_radii.radius_m.into(), + ..Default::default() + }, + icon: self.current_container().on.into(), + placeholder, + value, + selection, + }, + } + } + text_input::Status::Focused => { + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + + match class { + TextInput::Default => text_input::Style { + background: Color::from(bg).into(), + border: Border { + radius: palette.corner_radii.radius_s.into(), + width: 1.0, + color: palette.accent.base.into(), + }, + icon: self.current_container().on.into(), + placeholder, + value, + selection, + }, + TextInput::Search => text_input::Style { + background: Color::from(bg).into(), + border: Border { + radius: palette.corner_radii.radius_m.into(), + ..Default::default() + }, + icon: self.current_container().on.into(), + placeholder, + value, + selection, + }, + } + } + text_input::Status::Disabled => { + appearance.background = match appearance.background { + Background::Color(color) => Background::Color(Color { + a: color.a * 0.5, + ..color + }), + Background::Gradient(gradient) => { + Background::Gradient(gradient.scale_alpha(0.5)) + } + }; + appearance.border.color.a /= 2.; + appearance.icon.a /= 2.; + appearance.placeholder.a /= 2.; + appearance.value.a /= 2.; + appearance + } } } - - fn hovered(&self, style: &Self::Style) -> text_input::Appearance { - let palette = self.cosmic(); - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; - - match style { - TextInput::Default => text_input::Appearance { - background: Color::from(bg).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: Border { - radius: palette.corner_radii.radius_m.into(), - ..Default::default() - }, - icon_color: self.current_container().on.into(), - }, - } - } - - fn focused(&self, style: &Self::Style) -> text_input::Appearance { - let palette = self.cosmic(); - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; - - match style { - TextInput::Default => text_input::Appearance { - background: Color::from(bg).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: Border { - radius: palette.corner_radii.radius_m.into(), - ..Default::default() - }, - icon_color: self.current_container().on.into(), - }, - } - } - - fn placeholder_color(&self, _style: &Self::Style) -> Color { - 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 { - let palette = self.cosmic(); - - palette.palette.neutral_9.into() - } - - fn selection_color(&self, _style: &Self::Style) -> Color { - let palette = self.cosmic(); - - palette.accent.base.into() - } - - fn disabled_color(&self, _style: &Self::Style) -> Color { - let palette = self.cosmic(); - let mut neutral_9 = palette.palette.neutral_9; - neutral_9.alpha = 0.5; - neutral_9.into() - } - - fn disabled(&self, style: &Self::Style) -> text_input::Appearance { - self.active(style) - } } -impl crate::widget::card::style::StyleSheet for Theme { - fn default(&self) -> crate::widget::card::style::Appearance { - let cosmic = self.cosmic(); +// TODO card +// impl crate::widget::card::style::StyleSheet for Theme { +// fn default(&self) -> crate::widget::card::style::Style { +// let cosmic = self.cosmic(); - match self.layer { - cosmic_theme::Layer::Background => crate::widget::card::style::Appearance { - card_1: Background::Color(cosmic.background.component.hover.into()), - card_2: Background::Color(cosmic.background.component.pressed.into()), - }, - cosmic_theme::Layer::Primary => crate::widget::card::style::Appearance { - card_1: Background::Color(cosmic.primary.component.hover.into()), - card_2: Background::Color(cosmic.primary.component.pressed.into()), - }, - cosmic_theme::Layer::Secondary => crate::widget::card::style::Appearance { - card_1: Background::Color(cosmic.secondary.component.hover.into()), - card_2: Background::Color(cosmic.secondary.component.pressed.into()), - }, - } - } -} +// match self.layer { +// cosmic_theme::Layer::Background => crate::widget::card::style::Style { +// card_1: Background::Color(cosmic.background.component.hover.into()), +// card_2: Background::Color(cosmic.background.component.pressed.into()), +// }, +// cosmic_theme::Layer::Primary => crate::widget::card::style::Style { +// card_1: Background::Color(cosmic.primary.component.hover.into()), +// card_2: Background::Color(cosmic.primary.component.pressed.into()), +// }, +// cosmic_theme::Layer::Secondary => crate::widget::card::style::Style { +// card_1: Background::Color(cosmic.secondary.component.hover.into()), +// card_2: Background::Color(cosmic.secondary.component.pressed.into()), +// }, +// } +// } +// } #[derive(Default)] -pub enum TextEditor { +pub enum TextEditor<'a> { #[default] Default, - Custom(Box>), + Custom(text_editor::StyleFn<'a, Theme>), } -impl iced_style::text_editor::StyleSheet for Theme { - type Style = TextEditor; +impl iced_widget::text_editor::Catalog for Theme { + type Class<'a> = TextEditor<'a>; - fn active(&self, style: &Self::Style) -> iced_style::text_editor::Appearance { - if let TextEditor::Custom(style) = style { - return style.active(self); + fn default<'a>() -> Self::Class<'a> { + TextEditor::default() + } + + fn style( + &self, + class: &Self::Class<'_>, + status: iced_widget::text_editor::Status, + ) -> iced_widget::text_editor::Style { + if let TextEditor::Custom(style) = class { + return style(self, status); } let cosmic = self.cosmic(); - iced_style::text_editor::Appearance { - background: iced::Color::from(cosmic.bg_color()).into(), - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - color: iced::Color::from(cosmic.bg_divider()), + + let selection = cosmic.accent.base.into(); + let value = cosmic.palette.neutral_9.into(); + let mut placeholder = cosmic.palette.neutral_9; + placeholder.alpha = 0.7; + let placeholder = placeholder.into(); + let icon = cosmic.background.on.into(); + + match status { + iced_widget::text_editor::Status::Active + | iced_widget::text_editor::Status::Hovered + | iced_widget::text_editor::Status::Disabled => iced_widget::text_editor::Style { + background: iced::Color::from(cosmic.bg_color()).into(), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.bg_divider()), + }, + icon, + placeholder, + value, + selection, + }, + iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style { + background: iced::Color::from(cosmic.bg_color()).into(), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.accent.base), + }, + icon, + placeholder, + value, + selection, }, } } - - 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: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - 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) - } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index 0469673..1cbd4ef 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -10,8 +10,6 @@ mod dropdown; pub mod iced; #[doc(inline)] -pub use self::iced::Application; -#[doc(inline)] pub use self::iced::Checkbox; #[doc(inline)] pub use self::iced::Container; diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index c918f8c..bfc9a78 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -9,9 +9,10 @@ use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; use iced_core::widget::Tree; -use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget}; -pub use iced_style::container::{Appearance, StyleSheet}; +use iced_widget::container; +pub use iced_widget::container::{Catalog, Style}; pub fn aspect_ratio_container<'a, Message: 'static, T>( content: T, @@ -117,22 +118,22 @@ where /// Centers the contents in the horizontal axis of the [`Container`]. #[must_use] - pub fn center_x(mut self) -> Self { - self.container = self.container.center_x(); + pub fn center_x(mut self, width: Length) -> Self { + self.container = self.container.center_x(width); self } /// Centers the contents in the vertical axis of the [`Container`]. #[must_use] - pub fn center_y(mut self) -> Self { - self.container = self.container.center_y(); + pub fn center_y(mut self, height: Length) -> Self { + self.container = self.container.center_y(height); self } /// Sets the style of the [`Container`]. #[must_use] - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.container = self.container.style(style); + pub fn class(mut self, style: impl Into>) -> Self { + self.container = self.container.class(style); self } } @@ -173,9 +174,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container.operate(tree, layout, renderer, operation); } @@ -241,8 +240,9 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer) + self.container.overlay(tree, layout, renderer, translation) } } diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs new file mode 100644 index 0000000..553f9ea --- /dev/null +++ b/src/widget/autosize.rs @@ -0,0 +1,276 @@ +//! Autosize Container, which will resize the window to its contents. + +use cctk::sctk::shell::xdg::window; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::{Id, Tree}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; +pub use iced_widget::container::{Catalog, Style}; + +pub fn autosize<'a, Message: 'static, Theme, E>( + content: E, + id: Id, +) -> Autosize<'a, Message, Theme, crate::Renderer> +where + E: Into>, + Theme: iced_widget::container::Catalog, + ::Class<'a>: From>, +{ + Autosize::new(content, id) +} + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct Autosize<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + id: Id, + limits: layout::Limits, + auto_width: bool, + auto_height: bool, +} + +impl<'a, Message, Theme, Renderer> Autosize<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + /// Creates an empty [`IdContainer`]. + pub(crate) fn new(content: T, id: Id) -> Self + where + T: Into>, + { + Autosize { + content: content.into(), + id, + limits: layout::Limits::NONE, + auto_width: true, + auto_height: true, + } + } + + pub fn limits(mut self, limits: layout::Limits) -> Self { + self.limits = limits; + self + } + + pub fn auto_width(mut self, auto_width: bool) -> Self { + self.auto_width = auto_width; + self + } + + pub fn auto_height(mut self, auto_height: bool) -> Self { + self.auto_height = auto_height; + self + } + + pub fn max_width(mut self, v: f32) -> Self { + self.limits = self.limits.max_width(v); + self + } + + pub fn max_height(mut self, v: f32) -> Self { + self.limits = self.limits.max_height(v); + self + } + + pub fn min_width(mut self, v: f32) -> Self { + self.limits = self.limits.min_width(v); + self + } + + pub fn min_height(mut self, v: f32) -> Self { + self.limits = self.limits.min_height(v); + self + } +} + +impl<'a, Message, Theme, Renderer> Widget + for Autosize<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.children[0].diff(&mut self.content); + } + + fn size(&self) -> iced_core::Size { + self.content.as_widget().size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + _limits: &layout::Limits, + ) -> layout::Node { + let mut limits = self.limits; + let min = self.limits.min(); + let max = self.limits.max(); + if self.auto_width { + limits.min_width(min.width); + limits.max_width(max.width); + } + if self.auto_height { + limits.min_height(min.height); + limits.max_height(max.height); + } + let node = self + .content + .as_widget() + .layout(&mut tree.children[0], renderer, &self.limits); + let size = node.size(); + layout::Node::with_children(size, vec![node]) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation<()>, + ) { + operation.container(Some(&self.id), layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + #[cfg(feature = "wayland")] + if matches!( + event, + Event::PlatformSpecific(event::PlatformSpecific::Wayland( + event::wayland::Event::RequestResize + )) + ) { + let bounds = layout.bounds().size(); + clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.)); + } + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let content_layout = layout.children().next().unwrap(); + self.content.as_widget().mouse_interaction( + &tree.children[0], + content_layout, + cursor_position, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let content_layout = layout.children().next().unwrap(); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + content_layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + translation, + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + let content_layout = layout.children().next().unwrap(); + self.content.as_widget().drag_destinations( + &state.children[0], + content_layout, + renderer, + dnd_rectangles, + ); + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: crate::widget::Id) { + self.id = id; + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: 'a + iced_core::Renderer, + Theme: 'a, +{ + fn from(c: Autosize<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> { + Element::new(c) + } +} diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 311f8eb..0322d58 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Builder, Style}; +use super::{Builder, ButtonClass}; use crate::widget::{ icon::{self, Handle}, tooltip, @@ -48,7 +48,7 @@ impl<'a, Message> Button<'a, Message> { line_height: 20, font_size: 14, font_weight: Weight::Normal, - style: Style::Icon, + class: ButtonClass::Icon, variant: icon, } } @@ -121,7 +121,7 @@ impl<'a, Message> Button<'a, Message> { pub fn vertical(mut self, vertical: bool) -> Self { self.variant.vertical = vertical; - self.style = Style::IconVertical; + self.class = ButtonClass::IconVertical; self } } @@ -157,7 +157,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) - .align_items(Alignment::Center) + .align_x(Alignment::Center) .apply(super::custom) } else { crate::widget::row::with_children(content) @@ -165,7 +165,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .width(builder.width) .height(builder.height) .spacing(builder.spacing) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .apply(super::custom) }; @@ -174,18 +174,22 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .id(builder.id) .on_press_maybe(builder.on_press) .selected(builder.variant.selected) - .style(builder.style); + .class(builder.class); if builder.tooltip.is_empty() { button.into() } else { - tooltip(button, builder.tooltip, tooltip::Position::Top) - .size(builder.font_size) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }) - .into() + tooltip( + button, + crate::widget::text(builder.tooltip) + .size(builder.font_size) + .font(crate::font::Font { + weight: builder.font_weight, + ..crate::font::default() + }), + tooltip::Position::Top, + ) + .into() } } } diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index d93844c..a0508aa 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -42,7 +42,7 @@ impl<'a, Message> Button<'a, Message> { line_height: 20, font_size: 14, font_weight: Weight::Normal, - style: Style::Image, + class: crate::theme::style::Button::Image, variant, } } @@ -80,7 +80,7 @@ where .selected(builder.variant.selected) .id(builder.id) .on_press_maybe(builder.on_press) - .style(builder.style) + .class(builder.class) .into() } } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index e62b05b..77527ec 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -4,7 +4,7 @@ //! Hyperlink button widget use super::Builder; -use super::Style; +use super::ButtonClass; use crate::prelude::*; use crate::widget::icon::{self, Handle}; use crate::widget::{button, row, tooltip}; @@ -44,7 +44,7 @@ impl<'a, Message> Button<'a, Message> { line_height: 20, font_size: 14, font_weight: Weight::Normal, - style: Style::Link, + class: ButtonClass::Link, variant: link, } } @@ -81,23 +81,27 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .width(builder.width) .height(builder.height) .spacing(builder.spacing) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .apply(button::custom) .padding(0) .id(builder.id) .on_press_maybe(builder.on_press.take()) - .style(builder.style); + .class(builder.class); if builder.tooltip.is_empty() { button.into() } else { - tooltip(button, builder.tooltip, tooltip::Position::Top) - .size(builder.font_size) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }) - .into() + tooltip( + button, + crate::widget::text(builder.tooltip) + .size(builder.font_size) + .font(crate::font::Font { + weight: builder.font_weight, + ..crate::font::default() + }), + tooltip::Position::Top, + ) + .into() } } } diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index 4a83b12..6bf6338 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -3,7 +3,7 @@ //! Button widgets for COSMIC applications. -pub use crate::theme::Button as Style; +pub use crate::theme::Button as ButtonClass; pub mod link; use derive_setters::Setters; @@ -26,7 +26,7 @@ pub use image::Button as ImageButton; mod style; #[doc(inline)] -pub use style::{Appearance, StyleSheet}; +pub use style::{Catalog, Style}; mod text; #[doc(inline)] @@ -105,7 +105,7 @@ pub struct Builder<'a, Message, Variant> { font_weight: Weight, /// The preferred style of the button. - style: Style, + class: ButtonClass, #[setters(skip)] variant: Variant, diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs index 87f2777..3610011 100644 --- a/src/widget/button/style.rs +++ b/src/widget/button/style.rs @@ -9,7 +9,7 @@ use crate::theme::THEME; /// The appearance of a button. #[must_use] #[derive(Debug, Clone, Copy)] -pub struct Appearance { +pub struct Style { /// The amount of offset to apply to the shadow of the button. pub shadow_offset: Vector, @@ -41,7 +41,7 @@ pub struct Appearance { pub text_color: Option, } -impl Appearance { +impl Style { // TODO: `Radius` is not `const fn` compatible. pub fn new() -> Self { let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; @@ -60,33 +60,34 @@ impl Appearance { } } -impl std::default::Default for Appearance { +impl std::default::Default for Style { fn default() -> Self { Self::new() } } +// TODO update to match other styles /// A set of rules that dictate the style of a button. -pub trait StyleSheet { +pub trait Catalog { /// The supported style of the [`StyleSheet`]. - type Style: Default; + type Class: Default; /// Produces the active [`Appearance`] of a button. - fn active(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance; + fn active(&self, focused: bool, selected: bool, style: &Self::Class) -> Style; /// Produces the disabled [`Appearance`] of a button. - fn disabled(&self, style: &Self::Style) -> Appearance; + fn disabled(&self, style: &Self::Class) -> Style; /// [`Appearance`] when the button is the target of a DND operation. - fn drop_target(&self, style: &Self::Style) -> Appearance { + fn drop_target(&self, style: &Self::Class) -> Style { self.hovered(false, false, style) } /// Produces the hovered [`Appearance`] of a button. - fn hovered(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance; + fn hovered(&self, focused: bool, selected: bool, style: &Self::Class) -> Style; /// Produces the pressed [`Appearance`] of a button. - fn pressed(&self, focused: bool, selected: bool, style: &Self::Style) -> Appearance; + fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style; /// Background color of the selection indicator fn selection_background(&self) -> Background; diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 0a59976..3047624 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Builder, Style}; +use super::{Builder, ButtonClass, Style}; use crate::widget::{icon, row, tooltip}; use crate::{ext::CollectionWidget, Element}; use apply::Apply; @@ -14,14 +14,14 @@ pub type Button<'a, Message> = Builder<'a, Message, Text>; pub fn destructive<'a, Message>(label: impl Into>) -> Button<'a, Message> { Button::new(Text::new()) .label(label) - .style(Style::Destructive) + .class(ButtonClass::Destructive) } /// A text button with the suggested style pub fn suggested<'a, Message>(label: impl Into>) -> Button<'a, Message> { Button::new(Text::new()) .label(label) - .style(Style::Suggested) + .class(ButtonClass::Suggested) } /// A text button with the standard style @@ -31,7 +31,9 @@ pub fn standard<'a, Message>(label: impl Into>) -> Button<'a, Messa /// A text button with the text style pub fn text<'a, Message>(label: impl Into>) -> Button<'a, Message> { - Button::new(Text::new()).label(label).style(Style::Text) + Button::new(Text::new()) + .label(label) + .class(ButtonClass::Text) } /// The text variant of a button. @@ -66,7 +68,7 @@ impl<'a, Message> Button<'a, Message> { line_height: 20, font_size: 14, font_weight: Weight::Normal, - style: Style::Standard, + class: ButtonClass::Standard, variant: text, } } @@ -125,23 +127,27 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .width(builder.width) .height(builder.height) .spacing(builder.spacing) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .apply(super::custom) .padding(0) .id(builder.id) .on_press_maybe(builder.on_press.take()) - .style(builder.style); + .class(builder.class); if builder.tooltip.is_empty() { button.into() } else { - tooltip(button, builder.tooltip, tooltip::Position::Top) - .size(builder.font_size) - .font(crate::font::Font { - weight: builder.font_weight, - ..crate::font::default() - }) - .into() + tooltip( + button, + crate::widget::text(builder.tooltip) + .size(builder.font_size) + .font(crate::font::Font { + weight: builder.font_weight, + ..crate::font::default() + }), + tooltip::Position::Top, + ) + .into() } } } diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 9dd969f..1c0ef25 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -7,7 +7,7 @@ //! A [`Button`] has some local [`State`]. use iced_runtime::core::widget::Id; -use iced_runtime::{keyboard, Command}; +use iced_runtime::{keyboard, task, Action, Task}; use iced_core::event::{self, Event}; use iced_core::renderer::{self, Quad, Renderer}; @@ -20,11 +20,11 @@ use iced_core::{overlay, Shadow}; use iced_core::{ Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, }; -use iced_renderer::core::widget::{operation, OperationOutputWrapper}; +use iced_renderer::core::widget::operation; use crate::theme::THEME; -pub use super::style::{Appearance, StyleSheet}; +pub use super::style::{Catalog, Style}; /// Internally defines different button widget variants. enum Variant { @@ -173,7 +173,7 @@ impl<'a, Message> Button<'a, Message> { } /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: crate::theme::Button) -> Self { + pub fn class(mut self, style: crate::theme::Button) -> Self { self.style = style; self } @@ -257,7 +257,7 @@ impl<'a, Message: 'a + Clone> Widget tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -470,10 +470,11 @@ impl<'a, Message: 'a + Clone> Widget selection_background, ); - iced_core::svg::Renderer::draw( + let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone()) + .color(icon_color); + iced_core::svg::Renderer::draw_svg( renderer, - crate::widget::common::object_select().clone(), - Some(icon_color), + svg_handle, Rectangle { width: 16.0, height: 16.0, @@ -498,11 +499,10 @@ impl<'a, Message: 'a + Clone> Widget }, selection_background, ); - - iced_core::svg::Renderer::draw( + let svg_handle = svg::Svg::new(close_icon.clone()).color(icon_color); + iced_core::svg::Renderer::draw_svg( renderer, - close_icon.clone(), - Some(icon_color), + svg_handle, Rectangle { width: 16.0, height: 16.0, @@ -533,11 +533,16 @@ impl<'a, Message: 'a + Clone> Widget tree: &'b mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, + mut translation: Vector, ) -> Option> { + let mut position = layout.bounds().position(); + translation.x += position.x; + translation.y += position.y; self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, + translation, ) } @@ -748,11 +753,11 @@ pub fn update<'a, Message: Clone>( pub fn draw( renderer: &mut Renderer, bounds: Rectangle, - styling: &super::style::Appearance, - draw_contents: impl FnOnce(&mut Renderer, &Appearance), + styling: &super::style::Style, + draw_contents: impl FnOnce(&mut Renderer, &Style), is_image: bool, ) where - Theme: super::style::StyleSheet, + Theme: super::style::Catalog, { let doubled_border_width = styling.border_width * 2.0; let doubled_outline_width = styling.outline_width * 2.0; @@ -909,9 +914,9 @@ pub fn mouse_interaction( } } -/// Produces a [`Command`] that focuses the [`Button`] with the given [`Id`]. -pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id)) +/// Produces a [`Task`] that focuses the [`Button`] with the given [`Id`]. +pub fn focus(id: Id) -> Task { + task::effect(Action::Widget(Box::new(operation::focusable::focus(id)))) } impl operation::Focusable for State { diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index f86841a..bcfcda6 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -86,7 +86,7 @@ where text(first_day_of_week.to_string()) .size(12) .width(Length::Fixed(36.0)) - .horizontal_alignment(Horizontal::Center), + .align_x(Horizontal::Center), ); first_day_of_week = first_day_of_week.succ(); @@ -138,17 +138,17 @@ fn date_button( on_select: &dyn Fn(NaiveDate) -> Message, ) -> crate::widget::Button<'static, Message> { let style = if is_day { - button::Style::Suggested + button::ButtonClass::Suggested } else { - button::Style::Text + button::ButtonClass::Text }; let button = button::custom( text(format!("{}", date.day())) - .horizontal_alignment(Horizontal::Center) - .vertical_alignment(Vertical::Center), + .align_x(Horizontal::Center) + .align_y(Vertical::Center), ) - .style(style) + .class(style) .height(Length::Fixed(36.0)) .width(Length::Fixed(36.0)); diff --git a/src/widget/card/style.rs b/src/widget/card/style.rs index 1a3d293..d73ef8f 100644 --- a/src/widget/card/style.rs +++ b/src/widget/card/style.rs @@ -5,12 +5,12 @@ use iced_core::{Background, Color}; /// Appearance of the cards. #[derive(Clone, Copy)] -pub struct Appearance { +pub struct Style { pub card_1: Background, pub card_2: Background, } -impl Default for Appearance { +impl Default for Style { fn default() -> Self { Self { card_1: Background::Color(Color::WHITE), @@ -20,7 +20,7 @@ impl Default for Appearance { } /// Defines the [`Appearance`] of a cards. -pub trait StyleSheet { +pub trait Catalog { /// The default [`Appearance`] of the cards. - fn default(&self) -> Appearance; + fn default(&self) -> Style; } diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index d7f42ad..0419959 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -10,10 +10,10 @@ use std::time::{Duration, Instant}; use crate::theme::iced::Slider; use crate::theme::{Button, THEME}; -use crate::widget::{container, segmented_button::Entity, slider}; +use crate::widget::{button::Catalog, container, segmented_button::Entity, slider}; use crate::Element; use derive_setters::Setters; -use iced::Command; +use iced::Task; use iced_core::event::{self, Event}; use iced_core::gradient::{ColorStop, Linear}; use iced_core::renderer::Quad; @@ -23,12 +23,11 @@ use iced_core::{ Rectangle, Renderer, Shadow, Shell, Size, Vector, Widget, }; -use iced_style::slider::{HandleShape, RailBackground}; +use iced_widget::slider::{HandleShape, RailBackground}; use iced_widget::{canvas, column, horizontal_space, row, scrollable, vertical_space, Row}; use lazy_static::lazy_static; use palette::{FromColor, RgbHue}; -use super::button::StyleSheet; use super::divider::horizontal; use super::icon::{self, from_name}; use super::segmented_button::{self, SingleSelect}; @@ -135,7 +134,7 @@ impl ColorPickerModel { ) } - pub fn update(&mut self, update: ColorPickerUpdate) -> Command { + pub fn update(&mut self, update: ColorPickerUpdate) -> Task { match update { ColorPickerUpdate::ActiveColor(c) => { self.must_clear_cache.store(true, Ordering::SeqCst); @@ -206,7 +205,7 @@ impl ColorPickerModel { self.copied_at = None; } }; - Command::none() + Task::none() } #[must_use] @@ -298,7 +297,7 @@ where .width(self.width), // canvas with gradient for the current color // still needs the canvas and the handle to be drawn on it - container(vertical_space(self.height)) + container(vertical_space().height(self.height)) .width(self.width) .height(self.height), slider( @@ -310,16 +309,21 @@ where on_update(ColorPickerUpdate::ActiveColor(new)) } ) - .style(Slider::Custom { + .class(Slider::Custom { active: Rc::new(|t| { let cosmic = t.cosmic(); - let mut a = slider::StyleSheet::active(t, &Slider::default()); - a.rail.colors = RailBackground::Gradient { - gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), - auto_angle: true, - }; + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + // a.rail.colors = RailBackground::Gradient { + // gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), + // auto_angle: true, + // }; + a.rail.backgrounds = ( + Background::Color(Color::TRANSPARENT), + Background::Color(Color::TRANSPARENT), + ); a.rail.width = 8.0; - a.handle.color = Color::TRANSPARENT; + a.handle.background = Color::TRANSPARENT.into(); a.handle.shape = HandleShape::Circle { radius: 8.0 }; a.handle.border_color = cosmic.palette.neutral_10.into(); a.handle.border_width = 4.0; @@ -327,13 +331,15 @@ where }), hovered: Rc::new(|t| { let cosmic = t.cosmic(); - let mut a = slider::StyleSheet::active(t, &Slider::default()); - a.rail.colors = RailBackground::Gradient { - gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), - auto_angle: true, - }; + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + // a.rail.colors = RailBackground::Gradient { + // gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), + // auto_angle: true, + // }; + a.rail.backgrounds = (Color::TRANSPARENT.into(), Color::TRANSPARENT.into()); a.rail.width = 8.0; - a.handle.color = Color::TRANSPARENT; + a.handle.background = Color::TRANSPARENT.into(); a.handle.shape = HandleShape::Circle { radius: 8.0 }; a.handle.border_color = cosmic.palette.neutral_10.into(); a.handle.border_width = 4.0; @@ -341,13 +347,22 @@ where }), dragging: Rc::new(|t| { let cosmic = t.cosmic(); - let mut a = slider::StyleSheet::active(t, &Slider::default()); - a.rail.colors = RailBackground::Gradient { - gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), - auto_angle: true, - }; + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + // a.rail.backgrounds = ( + // RailBackground::Gradient(Gradient { + // gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), + // auto_angle: true, + // }), + // RailBackground::Gradient { + // gradient: Linear::new(Radians(0.0)).add_stops(HSV_RAINBOW.clone()), + // auto_angle: true, + // }, + // ); + a.rail.backgrounds = + (iced::Color::TRANSPARENT.into(), Color::TRANSPARENT.into()); a.rail.width = 8.0; - a.handle.color = Color::TRANSPARENT; + a.handle.background = Color::TRANSPARENT.into(); a.handle.shape = HandleShape::Circle { radius: 8.0 }; a.handle.border_color = cosmic.palette.neutral_10.into(); a.handle.border_width = 4.0; @@ -373,7 +388,7 @@ where from_name("edit-copy-symbolic").size(spacing.space_s).into(), )) .on_press(on_update(ColorPickerUpdate::Copied(Instant::now()))) - .style(Button::Text); + .class(Button::Text); match self.copied_at.take() { Some(t) if Instant::now().duration_since(t) > Duration::from_secs(2) => { @@ -381,13 +396,13 @@ where } Some(_) => tooltip( button, - copied_to_clipboard_label, + text(copied_to_clipboard_label), iced_widget::tooltip::Position::Bottom, ) .into(), None => tooltip( button, - copy_to_clipboard_label, + text(copy_to_clipboard_label), iced_widget::tooltip::Position::Bottom, ) .into(), @@ -431,7 +446,7 @@ where ) .width(self.width) .direction(iced_widget::scrollable::Direction::Horizontal( - scrollable::Properties::new().alignment(scrollable::Alignment::End), + scrollable::Scrollbar::new().anchor(scrollable::Anchor::End), )) }] .spacing(spacing.space_xxs), @@ -445,7 +460,7 @@ where button::custom( text(reset_to_default) .width(self.width) - .horizontal_alignment(iced_core::alignment::Horizontal::Center) + .align_x(iced_core::alignment::Horizontal::Center) ) .width(self.width) .on_press(on_update(ColorPickerUpdate::Reset)) @@ -461,18 +476,18 @@ where button::custom( text(cancel) .width(self.width) - .horizontal_alignment(iced_core::alignment::Horizontal::Center) + .align_x(iced_core::alignment::Horizontal::Center) ) .width(self.width) .on_press(on_update(ColorPickerUpdate::Cancel)), button::custom( text(save) .width(self.width) - .horizontal_alignment(iced_core::alignment::Horizontal::Center) + .align_x(iced_core::alignment::Horizontal::Center) ) .width(self.width) .on_press(on_update(ColorPickerUpdate::AppliedColor)) - .style(Button::Suggested) + .class(Button::Suggested) ] .spacing(spacing.space_xs) .width(self.width), @@ -589,7 +604,7 @@ where let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y); iced_core::Renderer::with_translation(renderer, translation, |renderer| { - canvas::Renderer::draw(renderer, vec![geo]); + iced_renderer::geometry::Renderer::draw_geometry(renderer, geo); }); let bounds = canvas_layout.bounds(); @@ -657,10 +672,11 @@ where state: &'b mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { self.inner .as_widget_mut() - .overlay(&mut state.children[0], layout, renderer) + .overlay(&mut state.children[0], layout, renderer, translation) } fn on_event( @@ -782,12 +798,12 @@ pub fn color_button<'a, Message: 'static>( let spacing = THEME.lock().unwrap().cosmic().spacing; button::custom(if color.is_some() { - Element::from(vertical_space(Length::Fixed(f32::from(spacing.space_s)))) + Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s)))) } else { Element::from(column![ - vertical_space(Length::FillPortion(6)), + vertical_space().height(Length::FillPortion(6)), row![ - horizontal_space(Length::FillPortion(6)), + horizontal_space().width(Length::FillPortion(6)), Icon::from( icon::from_name("list-add-symbolic") .prefer_svg(true) @@ -797,17 +813,17 @@ pub fn color_button<'a, Message: 'static>( .width(icon_portion) .height(Length::Fill) .content_fit(iced_core::ContentFit::Contain), - horizontal_space(Length::FillPortion(6)), + horizontal_space().width(Length::FillPortion(6)), ] .height(icon_portion) .width(Length::Fill), - vertical_space(Length::FillPortion(6)), + vertical_space().height(Length::FillPortion(6)), ]) }) .width(Length::Fixed(f32::from(spacing.space_s))) .height(Length::Fixed(f32::from(spacing.space_s))) .on_press_maybe(on_press) - .style(crate::theme::Button::Custom { + .class(crate::theme::Button::Custom { active: Box::new(move |focused, theme| { let cosmic = theme.cosmic(); @@ -817,7 +833,7 @@ pub fn color_button<'a, Message: 'static>( (0.0, Color::TRANSPARENT) }; let standard = theme.active(focused, false, &Button::Standard); - button::Appearance { + button::Style { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), @@ -834,7 +850,7 @@ pub fn color_button<'a, Message: 'static>( let cosmic = theme.cosmic(); let standard = theme.disabled(&Button::Standard); - button::Appearance { + button::Style { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), @@ -857,7 +873,7 @@ pub fn color_button<'a, Message: 'static>( }; let standard = theme.hovered(focused, false, &Button::Standard); - button::Appearance { + button::Style { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), @@ -880,7 +896,7 @@ pub fn color_button<'a, Message: 'static>( }; let standard = theme.pressed(focused, false, &Button::Standard); - button::Appearance { + button::Style { shadow_offset: Vector::default(), background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 434bf8d..cc220d1 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -4,13 +4,14 @@ use crate::Element; use iced::advanced::layout::{self, Layout}; -use iced::advanced::widget::{self, Operation, OperationOutputWrapper}; +use iced::advanced::widget::{self, Operation}; use iced::advanced::{overlay, renderer}; use iced::advanced::{Clipboard, Shell}; use iced::{event, mouse, Event, Point, Rectangle, Size}; use iced_core::Renderer; pub(super) struct Overlay<'a, 'b, Message> { + pub(crate) position: Point, pub(super) content: &'b mut Element<'a, Message>, pub(super) tree: &'b mut widget::Tree, pub(super) width: f32, @@ -21,13 +22,8 @@ impl<'a, 'b, Message> overlay::Overlay where Message: Clone, { - fn layout( - &mut self, - renderer: &crate::Renderer, - bounds: Size, - position: Point, - _translation: iced::Vector, - ) -> layout::Node { + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + let position = self.position; let limits = layout::Limits::new(Size::ZERO, bounds) .width(self.width) .height(bounds.height - 8.0 - position.y); @@ -98,7 +94,7 @@ where &mut self, layout: Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { self.content .as_widget_mut() @@ -122,8 +118,9 @@ where layout: Layout<'_>, renderer: &crate::Renderer, ) -> Option> { + let translation = iced::Vector::new(self.position.x, self.position.y); self.content .as_widget_mut() - .overlay(self.tree, layout, renderer) + .overlay(self.tree, layout, renderer, translation) } } diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 417885b..8e548f4 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -13,11 +13,9 @@ use iced_core::event::{self, Event}; use iced_core::widget::{Operation, Tree}; use iced_core::{ layout, mouse, overlay as iced_overlay, renderer, Clipboard, Layout, Length, Padding, - Rectangle, Shell, Widget, + Rectangle, Shell, Vector, Widget, }; -use iced_renderer::core::widget::OperationOutputWrapper; - #[must_use] pub struct ContextDrawer<'a, Message> { id: Option, @@ -48,19 +46,19 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { text::heading(header) .width(Length::FillPortion(1)) .height(Length::Fill) - .horizontal_alignment(alignment::Horizontal::Center) - .vertical_alignment(alignment::Vertical::Center), + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center), ) .push( button::text("Close") .trailing_icon(icon::from_name("go-next-symbolic")) .on_press(on_close) - .style(crate::theme::Button::Link) + .class(crate::theme::Button::Link) .apply(container) .width(Length::FillPortion(1)) .height(Length::Fill) .align_x(alignment::Horizontal::Right) - .center_y(), + .center_y(Length::Fill), ) // XXX must be done after pushing elements or it may be overwritten by size hints from contents .height(Length::Fixed(80.0)) @@ -84,7 +82,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { container( LayerContainer::new(pane) .layer(cosmic_theme::Layer::Primary) - .style(crate::style::Container::ContextDrawer) + .class(crate::style::Container::ContextDrawer) .width(Length::Fill) .height(Length::Fill) .max_width(max_width), @@ -159,7 +157,7 @@ impl<'a, Message: Clone> Widget for ContextDraw tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { self.content .as_widget() @@ -232,17 +230,20 @@ impl<'a, Message: Clone> Widget for ContextDraw tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, + translation: Vector, ) -> Option> { let bounds = layout.bounds(); - Some(iced_overlay::Element::new( - layout.position(), - Box::new(Overlay { - content: &mut self.drawer, - tree: &mut tree.children[1], - width: bounds.width, - }), - )) + let mut position = layout.position(); + position.x += translation.x; + position.y += translation.y; + + Some(iced_overlay::Element::new(Box::new(Overlay { + content: &mut self.drawer, + tree: &mut tree.children[1], + width: bounds.width, + position, + }))) } #[cfg(feature = "a11y")] diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 261779f..ce0c94e 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -8,7 +8,7 @@ use crate::widget::menu::{ }; use derive_setters::Setters; use iced::touch::Finger; -use iced::Event; +use iced::{Event, Vector}; use iced_core::widget::{tree, Tree, Widget}; use iced_core::{event, mouse, touch, Length, Point, Size}; use std::collections::HashSet; @@ -144,9 +144,7 @@ impl<'a, Message: Clone> Widget tree: &mut Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { self.content .as_widget() @@ -213,6 +211,7 @@ impl<'a, Message: Clone> Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, + translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); @@ -247,6 +246,7 @@ impl<'a, Message: Clone> Widget root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), style: &crate::theme::menu_bar::MenuBarStyle::Default, + position: Point::new(translation.x, translation.y), } .overlay(), ) diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 7e583d2..014b738 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -72,11 +72,13 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes let mut content_col = widget::column::with_capacity(3 + dialog.controls.len() * 2); content_col = content_col.push(widget::text::title3(dialog.title)); if let Some(body) = dialog.body { - content_col = content_col.push(widget::vertical_space(Length::Fixed(space_xxs.into()))); + content_col = + content_col.push(widget::vertical_space().height(Length::Fixed(space_xxs.into()))); content_col = content_col.push(widget::text::body(body)); } for control in dialog.controls { - content_col = content_col.push(widget::vertical_space(Length::Fixed(space_s.into()))); + content_col = + content_col.push(widget::vertical_space().height(Length::Fixed(space_s.into()))); content_col = content_col.push(control); } @@ -90,7 +92,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(button) = dialog.tertiary_action { button_row = button_row.push(button); } - button_row = button_row.push(widget::horizontal_space(Length::Fill)); + button_row = button_row.push(widget::horizontal_space().width(Length::Fill)); if let Some(button) = dialog.secondary_action { button_row = button_row.push(button); } @@ -103,7 +105,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes widget::column::with_children(vec![content_row.into(), button_row.into()]) .spacing(space_l), ) - .style(style::Container::Dialog) + .class(style::Container::Dialog) .padding(space_m) .width(Length::Fixed(570.0)), ) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 5cb3271..6afac4d 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -3,6 +3,8 @@ use std::{ sync::atomic::{AtomicU64, Ordering}, }; +use iced::Vector; + use crate::{ iced::{ clipboard::{ @@ -280,9 +282,7 @@ impl<'a, Message: 'static> Widget tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container .as_widget() @@ -496,10 +496,11 @@ impl<'a, Message: 'static> Widget tree: &'b mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { self.container .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer) + .overlay(&mut tree.children[0], layout, renderer, translation) } fn drag_destinations( diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 03de37e..8032812 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,69 +1,77 @@ use std::any::Any; +use iced_core::window; + use crate::{ iced::{ clipboard::dnd::{DndAction, DndEvent, SourceEvent}, - event, mouse, overlay, Event, Length, Point, Rectangle, + event, mouse, overlay, Event, Length, Point, Rectangle, Vector, }, iced_core::{ self, layout, renderer, widget::{tree, Tree}, Clipboard, Shell, }, - iced_style, widget::{container, Id, Widget}, Element, }; pub fn dnd_source< 'a, - Message: 'static, - AppMessage: 'static, + Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + Send + 'static, >( child: impl Into>, -) -> DndSource<'a, Message, AppMessage, D> { +) -> DndSource<'a, Message, D> { DndSource::new(child) } -pub struct DndSource<'a, Message, AppMessage, D> { +pub struct DndSource<'a, Message, D> { id: Id, action: DndAction, container: Element<'a, Message>, + window: Option, drag_content: Option D>>, - drag_icon: Option (Element<'static, AppMessage>, tree::State)>>, + drag_icon: Option (Element<'static, ()>, tree::State)>>, + on_start: Option, + on_cancelled: Option, + on_finish: Option, drag_threshold: f32, - _phantom: std::marker::PhantomData, } impl< 'a, - Message: 'static, - AppMessage: 'static, + Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, - > DndSource<'a, Message, AppMessage, D> + > DndSource<'a, Message, D> { pub fn new(child: impl Into>) -> Self { Self { id: Id::unique(), + window: None, action: DndAction::Copy | DndAction::Move, container: container(child).into(), drag_content: None, drag_icon: None, drag_threshold: 8.0, - _phantom: std::marker::PhantomData, + on_start: None, + on_cancelled: None, + on_finish: None, } } pub fn with_id(child: impl Into>, id: Id) -> Self { Self { id, + window: None, action: DndAction::Copy | DndAction::Move, container: container(child).into(), drag_content: None, drag_icon: None, drag_threshold: 8.0, - _phantom: std::marker::PhantomData, + on_start: None, + on_cancelled: None, + on_finish: None, } } @@ -82,7 +90,7 @@ impl< #[must_use] pub fn drag_icon( mut self, - f: impl Fn() -> (Element<'static, AppMessage>, tree::State) + 'static, + f: impl Fn() -> (Element<'static, ()>, tree::State) + 'static, ) -> Self { self.drag_icon = Some(Box::new(f)); self @@ -98,10 +106,15 @@ impl< let Some(content) = self.drag_content.as_ref().map(|f| f()) else { return; }; + iced_core::clipboard::start_dnd( clipboard, false, - Some(iced_core::clipboard::DndSource::Widget(self.id.clone())), + if let Some(window) = self.window.as_ref() { + Some(iced_core::clipboard::DndSource::Surface(window.clone())) + } else { + Some(iced_core::clipboard::DndSource::Widget(self.id.clone())) + }, self.drag_icon.as_ref().map(|f| { let (icon, state) = f(); ( @@ -116,14 +129,33 @@ impl< self.action, ); } + + pub fn on_start(mut self, on_start: Option) -> Self { + self.on_start = on_start; + self + } + + pub fn on_cancel(mut self, on_cancelled: Option) -> Self { + self.on_cancelled = on_cancelled; + self + } + + pub fn on_finish(mut self, on_finish: Option) -> Self { + self.on_finish = on_finish; + self + } + + pub fn window(mut self, window: window::Id) -> Self { + self.window = Some(window); + self + } } impl< 'a, - Message: 'static, - AppMessage: 'static, + Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, - > Widget for DndSource<'a, Message, AppMessage, D> + > Widget for DndSource<'a, Message, D> { fn children(&self) -> Vec { vec![Tree::new(&self.container)] @@ -165,9 +197,7 @@ impl< tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id)); operation.container(Some(&self.id), layout.bounds(), &mut |operation| { @@ -210,7 +240,6 @@ impl< } state.left_pressed_position = Some(position); - // dbg!(&state, &self.id); return event::Status::Captured; } } @@ -229,8 +258,10 @@ impl< return ret; } if let Some(left_pressed_position) = state.left_pressed_position { - // dbg!(&state); if position.distance(left_pressed_position) > self.drag_threshold { + if let Some(on_start) = self.on_start.as_ref() { + shell.publish(on_start.clone()) + } self.start_dnd(clipboard, state.cached_bounds); state.is_dragging = true; state.left_pressed_position = None; @@ -249,8 +280,21 @@ impl< } _ => return ret, }, - Event::Dnd(DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished)) => { + Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { if state.is_dragging { + if let Some(m) = self.on_cancelled.as_ref() { + shell.publish(m.clone()); + } + state.is_dragging = false; + return event::Status::Captured; + } + return ret; + } + Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => { + if state.is_dragging { + if let Some(m) = self.on_finish.as_ref() { + shell.publish(m.clone()); + } state.is_dragging = false; return event::Status::Captured; } @@ -308,10 +352,11 @@ impl< tree: &'b mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { self.container .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer) + .overlay(&mut tree.children[0], layout, renderer, translation) } fn drag_destinations( @@ -319,7 +364,7 @@ impl< state: &Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.container.as_widget().drag_destinations( &state.children[0], @@ -340,12 +385,11 @@ impl< impl< 'a, - Message: 'static, - AppMessage: 'static, + Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, - > From> for Element<'a, Message> + > From> for Element<'a, Message> { - fn from(e: DndSource<'a, Message, AppMessage, D>) -> Element<'a, Message> { + fn from(e: DndSource<'a, Message, D>) -> Element<'a, Message> { Element::new(e) } } diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index e3c056f..73204f3 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -97,7 +97,7 @@ impl<'a, S: AsRef, Message: 'a> Menu<'a, S, Message> { position: Point, target_height: f32, ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { - overlay::Element::new(position, Box::new(Overlay::new(self, target_height))) + overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) } } @@ -129,10 +129,15 @@ struct Overlay<'a, Message> { width: f32, target_height: f32, style: (), + position: Point, } impl<'a, Message: 'a> Overlay<'a, Message> { - pub fn new>(menu: Menu<'a, S, Message>, target_height: f32) -> Self { + pub fn new>( + menu: Menu<'a, S, Message>, + target_height: f32, + position: Point, + ) -> Self { let Menu { state, options, @@ -160,7 +165,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { container = container .padding(padding) - .style(crate::style::Container::Dropdown); + .class(crate::style::Container::Dropdown); state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); @@ -170,6 +175,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { width, target_height, style, + position, } } } @@ -177,13 +183,8 @@ impl<'a, Message: 'a> Overlay<'a, Message> { impl<'a, Message> iced_core::Overlay for Overlay<'a, Message> { - fn layout( - &mut self, - renderer: &crate::Renderer, - bounds: Size, - position: Point, - _translation: iced::Vector, - ) -> layout::Node { + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + let position = self.position; let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -447,10 +448,13 @@ impl<'a, S: AsRef, Message> Widget appearance.selected_background, ); - svg::Renderer::draw( + let svg_handle = + iced_core::Svg::new(crate::widget::common::object_select().clone()) + .color(appearance.selected_text_color) + .border_radius(appearance.border_radius); + svg::Renderer::draw_svg( renderer, - crate::widget::common::object_select().clone(), - Some(appearance.selected_text_color), + svg_handle, Rectangle { x: item_x + item_width - 16.0 - 8.0, y: bounds.y + (bounds.height / 2.0 - 8.0), @@ -494,7 +498,7 @@ impl<'a, S: AsRef, Message> Widget text::Renderer::fill_text( renderer, Text { - content: option.as_ref(), + content: option.as_ref().to_string(), bounds: bounds.size(), size: Pixels(text_size), line_height: self.text_line_height, @@ -502,7 +506,7 @@ impl<'a, S: AsRef, Message> Widget horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, bounds.position(), color, diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 3710698..5e3aa6a 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -97,7 +97,7 @@ where position: Point, target_height: f32, ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { - overlay::Element::new(position, Box::new(Overlay::new(self, target_height))) + overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) } } @@ -129,12 +129,14 @@ struct Overlay<'a, Message> { width: f32, target_height: f32, style: (), + position: Point, } impl<'a, Message: 'a> Overlay<'a, Message> { pub fn new, Item: Clone + PartialEq>( menu: Menu<'a, S, Item, Message>, target_height: f32, + position: Point, ) -> Self { let Menu { state, @@ -163,7 +165,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { container = container .padding(padding) - .style(crate::style::Container::Dropdown); + .class(crate::style::Container::Dropdown); state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); @@ -173,6 +175,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { width, target_height, style, + position, } } } @@ -180,13 +183,8 @@ impl<'a, Message: 'a> Overlay<'a, Message> { impl<'a, Message> iced_core::Overlay for Overlay<'a, Message> { - fn layout( - &mut self, - renderer: &crate::Renderer, - bounds: Size, - position: Point, - _translation: iced::Vector, - ) -> layout::Node { + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + let position = self.position; let space_below = bounds.height - (position.y + self.target_height); let space_above = position.y; @@ -537,10 +535,13 @@ where appearance.selected_background, ); - svg::Renderer::draw( + let svg_handle = + svg::Svg::new(crate::widget::common::object_select().clone()) + .color(appearance.selected_text_color) + .border_radius(appearance.border_radius); + svg::Renderer::draw_svg( renderer, - crate::widget::common::object_select().clone(), - Some(appearance.selected_text_color), + svg_handle, Rectangle { x: item_x + item_width - 16.0 - 8.0, y: bounds.y + (bounds.height / 2.0 - 8.0), @@ -587,7 +588,7 @@ where text::Renderer::fill_text( renderer, Text { - content: option.as_ref(), + content: option.as_ref().to_string(), bounds: bounds.size(), size: iced::Pixels(text_size), line_height: self.text_line_height, @@ -595,7 +596,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, bounds.position(), color, @@ -636,7 +637,7 @@ where text::Renderer::fill_text( renderer, Text { - content: description.as_ref(), + content: description.as_ref().to_string(), bounds: bounds.size(), size: iced::Pixels(text_size), line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), @@ -644,7 +645,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, bounds.position(), appearance.description_color, diff --git a/src/widget/dropdown/multi/mod.rs b/src/widget/dropdown/multi/mod.rs index 2f3a68e..b496349 100644 --- a/src/widget/dropdown/multi/mod.rs +++ b/src/widget/dropdown/multi/mod.rs @@ -9,7 +9,7 @@ pub mod menu; pub use menu::Menu; mod widget; -pub use widget::{Appearance, Dropdown, StyleSheet}; +pub use widget::{Catalog, Dropdown, Style}; pub fn dropdown<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static>( model: &'a Model, diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index a97f03f..c40f876 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -9,10 +9,13 @@ use iced_core::event::{self, Event}; 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, Shadow}; -use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget}; +use iced_core::{ + Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, +}; +use iced_widget::pick_list; use std::ffi::OsStr; -pub use iced_widget::style::pick_list::{Appearance, StyleSheet}; +pub use iced_widget::pick_list::{Catalog, Style}; /// A widget for selecting a single value from a list of selections. #[derive(Setters)] @@ -98,7 +101,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> for (_, item) in &list.options { state .selections - .push((item.clone(), crate::Paragraph::new())); + .push((item.clone(), crate::Plain::default())); } } } @@ -182,6 +185,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> tree: &'b mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); @@ -216,8 +220,8 @@ pub struct State { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, - selections: Vec<(Item, crate::Paragraph)>, - descriptions: Vec, + selections: Vec<(Item, crate::Plain)>, + descriptions: Vec, } impl State { @@ -259,7 +263,7 @@ pub fn layout( text_size: f32, text_line_height: text::LineHeight, font: Option, - selection: Option<(&str, &mut crate::Paragraph)>, + selection: Option<(&str, &mut crate::Plain)>, ) -> layout::Node { use std::f32; @@ -267,7 +271,7 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 { + let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 { paragraph.update(Text { content: label, bounds: Size::new(f32::MAX, f32::MAX), @@ -277,7 +281,7 @@ pub fn layout( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); paragraph.min_width().round() }; @@ -410,7 +414,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static ) .width({ let measure = - |label: &str, paragraph: &mut crate::Paragraph, line_height: text::LineHeight| { + |label: &str, paragraph: &mut crate::Plain, line_height: text::LineHeight| { paragraph.update(Text { content: label, bounds: Size::new(f32::MAX, f32::MAX), @@ -420,7 +424,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); paragraph.min_width().round() }; @@ -433,7 +437,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static let paragraph = if state.descriptions.len() > desc_count { &mut state.descriptions[desc_count] } else { - state.descriptions.push(crate::Paragraph::new()); + state.descriptions.push(crate::Plain::default()); state.descriptions.last_mut().unwrap() }; desc_count += 1; @@ -448,7 +452,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static None => { state .selections - .push((item.clone(), crate::Paragraph::new())); + .push((item.clone(), crate::Plain::default())); state.selections.len() - 1 } }; @@ -499,9 +503,9 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( let is_mouse_over = cursor.is_over(bounds); let style = if is_mouse_over { - theme.hovered(&()) + theme.style(&(), pick_list::Status::Hovered) } else { - theme.active(&()) + theme.style(&(), pick_list::Status::Active) }; iced_core::Renderer::fill_quad( @@ -515,10 +519,10 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( ); if let Some(handle) = state.icon.clone() { - svg::Renderer::draw( + let svg_handle = iced_core::Svg::new(handle).color(style.text_color); + svg::Renderer::draw_svg( renderer, - handle, - Some(style.text_color), + svg_handle, Rectangle { x: bounds.x + bounds.width - gap - 16.0, y: bounds.center_y() - 8.0, @@ -541,7 +545,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( text::Renderer::fill_text( renderer, Text { - content, + content: content.to_string(), size: iced::Pixels(text_size), line_height: text_line_height, font, @@ -549,7 +553,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, bounds.position(), style.text_color, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 971072c..80d5a00 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -5,16 +5,18 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; +use iced::Radians; use iced_core::event::{self, Event}; 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, Shadow}; -use iced_core::{Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Widget}; +use iced_core::{ + Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, +}; +use iced_widget::pick_list::{self, Catalog}; use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; -pub use iced_widget::style::pick_list::{Appearance, StyleSheet}; - /// A widget for selecting a single value from a list of selections. #[derive(Setters)] pub struct Dropdown<'a, S: AsRef, Message> { @@ -62,6 +64,29 @@ 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::Plain::default); + 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::default()), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + }); + } + } } impl<'a, S: AsRef, Message: 'a> Widget @@ -80,7 +105,7 @@ impl<'a, S: AsRef, Message: 'a> Widget, Message: 'a> Widget, Message: 'a> Widget, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); @@ -237,8 +263,8 @@ pub struct State { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option, - selections: Vec, hashes: Vec, + selections: Vec, } impl State { @@ -280,7 +306,7 @@ pub fn layout( text_size: f32, text_line_height: text::LineHeight, font: Option, - selection: Option<(&str, &mut crate::Paragraph)>, + selection: Option<(&str, &mut crate::Plain)>, ) -> layout::Node { use std::f32; @@ -288,7 +314,7 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = move |(label, paragraph): (_, &mut crate::Paragraph)| -> f32 { + let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 { paragraph.update(Text { content: label, bounds: Size::new(f32::MAX, f32::MAX), @@ -298,7 +324,7 @@ pub fn layout( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); paragraph.min_width().round() }; @@ -430,14 +456,14 @@ 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: &crate::Paragraph| -> f32 { selection_paragraph.min_width().round() }; selections .iter() .zip(state.selections.iter_mut()) - .map(|(label, selection)| measure(label.as_ref(), selection)) + .map(|(label, selection)| measure(label.as_ref(), selection.raw())) .fold(0.0, |next, current| current.max(next)) + gap + 16.0 @@ -477,9 +503,9 @@ pub fn draw<'a, S>( let is_mouse_over = cursor.is_over(bounds); let style = if is_mouse_over { - theme.hovered(&()) + theme.style(&(), pick_list::Status::Hovered) } else { - theme.active(&()) + theme.style(&(), pick_list::Status::Active) }; iced_core::Renderer::fill_quad( @@ -493,10 +519,11 @@ pub fn draw<'a, S>( ); if let Some(handle) = state.icon.clone() { - svg::Renderer::draw( + let svg_handle = svg::Svg::new(handle).color(style.text_color); + + svg::Renderer::draw_svg( renderer, - handle, - Some(style.text_color), + svg_handle, Rectangle { x: bounds.x + bounds.width - gap - 16.0, y: bounds.center_y() - 8.0, @@ -517,7 +544,7 @@ pub fn draw<'a, S>( text::Renderer::fill_text( renderer, Text { - content, + content: content.to_string(), size: iced::Pixels(text_size), line_height: text_line_height, font, @@ -525,7 +552,7 @@ pub fn draw<'a, S>( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, bounds.position(), style.text_color, diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 9f19ca1..40aee41 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -6,9 +6,9 @@ use derive_setters::Setters; use iced_core::event::{self, Event}; use iced_core::widget::{Operation, Tree}; use iced_core::{ - layout, mouse, overlay, renderer, Clipboard, Layout, Length, Padding, Rectangle, Shell, Widget, + layout, mouse, overlay, renderer, Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, + Widget, }; -use iced_renderer::core::widget::OperationOutputWrapper; /// Responsively generates rows and columns of widgets based on its dimmensions. #[derive(Setters)] @@ -132,7 +132,7 @@ impl<'a, Message: 'static + Clone> Widget tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -225,8 +225,9 @@ impl<'a, Message: 'static + Clone> Widget tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&mut self.children, tree, layout, renderer, translation) } #[cfg(feature = "a11y")] @@ -252,7 +253,7 @@ impl<'a, Message: 'static + Clone> Widget state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { for ((e, layout), state) in self .children diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index a043f19..961ffb9 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -7,9 +7,8 @@ use iced_core::event::{self, Event}; use iced_core::widget::{Operation, Tree}; use iced_core::{ layout, mouse, overlay, renderer, Alignment, Clipboard, Layout, Length, Padding, Rectangle, - Shell, Widget, + Shell, Vector, Widget, }; -use iced_renderer::core::widget::OperationOutputWrapper; /// Responsively generates rows and columns of widgets based on its dimmensions. #[must_use] @@ -154,7 +153,7 @@ impl<'a, Message: 'static + Clone> Widget for G tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -247,8 +246,9 @@ impl<'a, Message: 'static + Clone> Widget for G tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer) + overlay::from_children(&mut self.children, tree, layout, renderer, translation) } #[cfg(feature = "a11y")] @@ -274,7 +274,7 @@ impl<'a, Message: 'static + Clone> Widget for G state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { for ((e, layout), state) in self .children diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1f74510..d487f6b 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -6,7 +6,7 @@ use crate::{ext::CollectionWidget, widget, Element}; use apply::Apply; use derive_setters::Setters; use iced::Length; -use iced_core::{widget::tree, Widget}; +use iced_core::{widget::tree, Vector, Widget}; use std::borrow::Cow; #[must_use] @@ -221,9 +221,7 @@ impl<'a, Message: Clone + 'static> Widget, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { let child_tree = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); @@ -237,12 +235,16 @@ impl<'a, Message: Clone + 'static> Widget, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { let child_tree = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); - self.header_bar_inner - .as_widget_mut() - .overlay(child_tree, child_layout, renderer) + self.header_bar_inner.as_widget_mut().overlay( + child_tree, + child_layout, + renderer, + translation, + ) } fn drag_destinations( @@ -250,7 +252,7 @@ impl<'a, Message: Clone + 'static> Widget, renderer: &crate::Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { if let Some((child_tree, child_layout)) = state.children.iter().zip(layout.children()).next() @@ -274,7 +276,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let mut end = std::mem::take(&mut self.end); // Also packs the window controls at the very end. - end.push(widget::horizontal_space(Length::Fixed(12.0)).into()); + end.push(widget::horizontal_space().width(Length::Fixed(12.0)).into()); end.push(self.window_controls()); let height = match self.density.unwrap_or_else(crate::config::header_size) { @@ -288,7 +290,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { // If elements exist in the start region, append them here. .push( widget::row::with_children(start) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Left) .width(Length::Shrink), @@ -297,32 +299,32 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { // This will otherwise use the title as a widget if a title was defined. .push(if !center.is_empty() { widget::row::with_children(center) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Center) .width(Length::Fill) .into() } else if self.title.is_empty() { - widget::horizontal_space(Length::Fill).into() + widget::horizontal_space().width(Length::Fill).into() } else { self.title_widget() }) .push( widget::row::with_children(end) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::alignment::Horizontal::Right) .width(Length::Shrink), ) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .height(Length::Fixed(height)) .padding([0, 8]) .spacing(8) .apply(widget::container) - .style(crate::theme::Container::HeaderBar { + .class(crate::theme::Container::HeaderBar { focused: self.focused, }) - .center_y() + .center_y(Length::Shrink) .apply(widget::mouse_area); // Assigns a message to emit when the headerbar is dragged. @@ -350,10 +352,8 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { widget::text::heading(title) .apply(widget::container) - .center_x() - .center_y() - .width(Length::Fill) - .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) .into() } @@ -380,7 +380,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .padding(8) }; - icon.style(crate::theme::Button::HeaderBar) + icon.class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) @@ -405,8 +405,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { ) .spacing(8) .apply(widget::container) - .height(Length::Fill) - .center_y() + .center_y(Length::Fill) .into() } } diff --git a/src/widget/icon/handle.rs b/src/widget/icon/handle.rs index 1a13884..fe32590 100644 --- a/src/widget/icon/handle.rs +++ b/src/widget/icon/handle.rs @@ -55,7 +55,10 @@ pub fn from_raster_bytes( ) -> Handle { Handle { symbolic: false, - data: Data::Image(image::Handle::from_memory(bytes)), + data: match bytes.into() { + Cow::Owned(b) => Data::Image(image::Handle::from_bytes(b)), + Cow::Borrowed(b) => Data::Image(image::Handle::from_bytes(b)), + }, } } @@ -66,12 +69,14 @@ pub fn from_raster_pixels( pixels: impl Into> + std::convert::AsRef<[u8]> + std::marker::Send - + std::marker::Sync - + 'static, + + std::marker::Sync, ) -> Handle { Handle { symbolic: false, - data: Data::Image(image::Handle::from_pixels(width, height, pixels)), + data: match pixels.into() { + Cow::Owned(pixels) => Data::Image(image::Handle::from_bytes(pixels)), + Cow::Borrowed(pixels) => Data::Image(image::Handle::from_bytes(pixels)), + }, } } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 752125f..5fe630f 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -24,7 +24,7 @@ pub fn icon(handle: Handle) -> Icon { handle, height: None, size: 16, - style: crate::theme::Svg::default(), + class: crate::theme::Svg::default(), width: None, } } @@ -40,7 +40,7 @@ pub fn from_name(name: impl Into>) -> Named { pub struct Icon { #[setters(skip)] handle: Handle, - style: crate::theme::Svg, + class: crate::theme::Svg, pub(super) size: u16, content_fit: ContentFit, #[setters(strip_option)] @@ -86,7 +86,7 @@ impl Icon { let from_svg = |handle| { Svg::::new(handle) - .style(self.style.clone()) + .class(self.class.clone()) .width( self.width .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 2bd56b3..9be387a 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -4,8 +4,8 @@ use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; use iced_core::widget::{Id, Tree}; -use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Widget}; -pub use iced_style::container::{Appearance, StyleSheet}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; +pub use iced_widget::container::{Catalog, Style}; pub fn id_container<'a, Message: 'static, Theme, E>( content: E, @@ -13,8 +13,8 @@ pub fn id_container<'a, Message: 'static, Theme, E>( ) -> IdContainer<'a, Message, Theme, crate::Renderer> where E: Into>, - Theme: iced_style::container::StyleSheet, - ::Style: From, + Theme: iced_widget::container::Catalog, + ::Class<'a>: From>, { IdContainer::new(content, id) } @@ -83,9 +83,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { operation.container(Some(&self.id), layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -165,11 +163,13 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( &mut tree.children[0], layout.children().next().unwrap(), renderer, + translation, ) } @@ -178,7 +178,7 @@ where state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { let content_layout = layout.children().next().unwrap(); self.content.as_widget().drag_destinations( diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 7033e34..3f3e495 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -1,3 +1,4 @@ +use crate::Theme; use cosmic_theme::LayeredTheme; use iced::widget::Container; use iced_core::alignment; @@ -7,16 +8,14 @@ use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; use iced_core::widget::Tree; -use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; -pub use iced_style::container::{Appearance, StyleSheet}; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget}; +pub use iced_widget::container::{Catalog, Style}; -pub fn layer_container<'a, Message: 'static, Theme, E>( +pub fn layer_container<'a, Message: 'static, E>( content: E, -) -> LayerContainer<'a, Message, Theme, crate::Renderer> +) -> LayerContainer<'a, Message, crate::Renderer> where E: Into>, - Theme: iced_style::container::StyleSheet + LayeredTheme, - ::Style: From, { LayerContainer::new(content) } @@ -25,20 +24,18 @@ where /// /// It is normally used for alignment purposes. #[allow(missing_debug_implementations)] -pub struct LayerContainer<'a, Message, Theme, Renderer> +pub struct LayerContainer<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Theme: iced_style::container::StyleSheet + LayeredTheme, { layer: Option, container: Container<'a, Message, Theme, Renderer>, } -impl<'a, Message, Theme, Renderer> LayerContainer<'a, Message, Theme, Renderer> +impl<'a, Message, Renderer> LayerContainer<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Theme: iced_style::container::StyleSheet + LayeredTheme, - ::Style: From, + // iced_widget::container::Style: From, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T) -> Self @@ -55,7 +52,7 @@ where #[must_use] pub fn layer(mut self, layer: cosmic_theme::Layer) -> Self { self.layer = Some(layer); - self.style(match layer { + self.class(match layer { cosmic_theme::Layer::Background => crate::theme::Container::Background, cosmic_theme::Layer::Primary => crate::theme::Container::Primary, cosmic_theme::Layer::Secondary => crate::theme::Container::Secondary, @@ -113,31 +110,30 @@ where /// Centers the contents in the horizontal axis of the [`LayerContainer`]. #[must_use] - pub fn center_x(mut self) -> Self { - self.container = self.container.center_x(); + pub fn center_x(mut self, width: Length) -> Self { + self.container = self.container.center_x(width); self } /// Centers the contents in the vertical axis of the [`LayerContainer`]. #[must_use] - pub fn center_y(mut self) -> Self { - self.container = self.container.center_y(); + pub fn center_y(mut self, height: Length) -> Self { + self.container = self.container.center_y(height); self } /// Sets the style of the [`LayerContainer`]. #[must_use] - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.container = self.container.style(style); + pub fn class(mut self, style: impl Into>) -> Self { + self.container = self.container.class(style); self } } -impl<'a, Message, Theme, Renderer> Widget - for LayerContainer<'a, Message, Theme, Renderer> +impl<'a, Message, Renderer> Widget + for LayerContainer<'a, Message, Renderer> where Renderer: iced_core::Renderer, - Theme: iced_style::container::StyleSheet + LayeredTheme + Clone, { fn children(&self) -> Vec { self.container.children() @@ -173,9 +169,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container.operate(tree, layout, renderer, operation); } @@ -232,6 +226,7 @@ where } else { theme.clone() }; + self.container.draw( tree, renderer, @@ -248,8 +243,9 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer) + self.container.overlay(tree, layout, renderer, translation) } fn drag_destinations( @@ -257,7 +253,7 @@ where state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.container .drag_destinations(state, layout, renderer, dnd_rectangles); @@ -272,15 +268,14 @@ where } } -impl<'a, Message, Theme, Renderer> From> +impl<'a, Message, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, Renderer: 'a + iced_core::Renderer, - Theme: iced_style::container::StyleSheet + LayeredTheme + 'a + Clone, { fn from( - column: LayerContainer<'a, Message, Theme, Renderer>, + column: LayerContainer<'a, Message, Renderer>, ) -> Element<'a, Message, Theme, Renderer> { Element::new(column) } diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 082fc8d..18c32f4 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use iced_core::Padding; -use iced_style::container::StyleSheet; +use iced_widget::container::Catalog; use crate::{theme, widget::divider, Apply, Element}; @@ -14,7 +14,7 @@ pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { pub struct ListColumn<'a, Message> { spacing: u16, padding: Padding, - style: ::Style, + style: crate::theme::Container<'a>, children: Vec>, } @@ -23,7 +23,7 @@ impl<'a, Message: 'static> Default for ListColumn<'a, Message> { Self { spacing: theme::THEME.lock().unwrap().cosmic().spacing.space_xxs, padding: Padding::from(0), - style: ::Style::List, + style: crate::theme::Container::List, children: Vec::with_capacity(4), } } @@ -41,9 +41,11 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { } // Ensure a minimum height of 32. - let container = crate::widget::container(item) - .min_height(32) - .align_y(iced::alignment::Vertical::Center); + let container = iced::widget::row![ + crate::widget::container(item).align_y(iced::alignment::Vertical::Center), + crate::widget::vertical_space().height(iced::Length::Fixed(32.)) + ] + .align_y(iced::alignment::Vertical::Center); self.children.push(container.into()); self @@ -55,7 +57,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { } /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { + pub fn style(mut self, style: ::Class<'a>) -> Self { self.style = style; self } @@ -72,7 +74,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { .padding(self.padding) .apply(super::container) .padding([self.spacing, 8]) - .style(self.style) + .class(self.style) .into() } } diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index d94da27..9b844e2 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -13,6 +13,6 @@ pub fn container<'a, Message>( ) -> Container<'a, Message, crate::Theme, crate::Renderer> { super::container(content) .padding([16, 6]) - .style(crate::theme::Container::List) + .class(crate::theme::Container::List) .width(iced::Length::Fill) } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 257ccbf..1f11292 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -9,6 +9,7 @@ use super::{ }; use crate::style::menu_bar::StyleSheet; +use iced::{Point, Vector}; use iced_core::Border; use iced_widget::core::{ event, @@ -434,6 +435,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, _renderer: &Renderer, + translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); if !state.open { @@ -455,6 +457,7 @@ where root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(), path_highlight: self.path_highlight, style: &self.style, + position: Point::new(translation.x, translation.y), } .overlay(), ) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index bc0b772..19644b7 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -276,7 +276,7 @@ impl MenuBounds { let offset_bounds = Rectangle::new(offset_position, offset_size); let children_bounds = Rectangle::new(children_position, children_size); - let check_bounds = pad_rectangle(children_bounds, [bounds_expand; 4].into()); + let check_bounds = pad_rectangle(children_bounds, bounds_expand.into()); Self { child_positions, @@ -445,13 +445,14 @@ where pub(crate) root_bounds_list: Vec, pub(crate) path_highlight: Option, pub(crate) style: &'b ::Style, + pub(crate) position: Point, } impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer> where Renderer: renderer::Renderer, { pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, Renderer> { - overlay::Element::new(Point::ORIGIN, Box::new(self)) + overlay::Element::new(Box::new(self)) } } impl<'a, 'b, Message, Renderer> overlay::Overlay @@ -459,14 +460,9 @@ impl<'a, 'b, Message, Renderer> overlay::Overlay Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { // layout children + let position = self.position; let state = self.tree.state.downcast_mut::(); let overlay_offset = Point::ORIGIN - position; let tree_children = &mut self.tree.children; diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 65ebbd9..736ba2f 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -146,14 +146,14 @@ pub fn menu_button<'a, Message: 'a>( ) -> crate::widget::Button<'a, Message> { widget::button::custom( widget::Row::with_children(children) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .height(Length::Fill) .width(Length::Fill), ) .height(Length::Fixed(36.0)) .padding([4, 16]) .width(Length::Fill) - .style(theme::Button::MenuItem) + .class(theme::Button::MenuItem) } /// Represents a menu item that performs an action when selected or a separator between menu items. @@ -197,7 +197,7 @@ where { widget::button::custom(widget::text(label)) .padding([4, 12]) - .style(theme::Button::MenuRoot) + .class(theme::Button::MenuRoot) .into() } @@ -245,7 +245,7 @@ where let key = find_key(&action, key_binds); let menu_button = menu_button(vec![ widget::text(label).into(), - widget::horizontal_space(Length::Fill).into(), + widget::horizontal_space().width(Length::Fill).into(), widget::text(key).into(), ]) .on_press(action.message()); @@ -256,7 +256,7 @@ where let key = find_key(&action, key_binds); let menu_button = menu_button(vec![ widget::text(label).into(), - widget::horizontal_space(Length::Fill).into(), + widget::horizontal_space().width(Length::Fill).into(), widget::text(key).into(), ]); @@ -270,8 +270,8 @@ where widget::icon::from_name("object-select-symbolic") .size(16) .icon() - .style(theme::Svg::Custom(Rc::new(|theme| { - crate::iced_style::svg::Appearance { + .class(theme::Svg::Custom(Rc::new(|theme| { + iced_widget::svg::Style { color: Some(theme.cosmic().accent_color().into()), } }))) @@ -282,9 +282,9 @@ where }, widget::Space::with_width(Length::Fixed(8.0)).into(), widget::text(label) - .horizontal_alignment(iced::alignment::Horizontal::Left) + .align_x(iced::alignment::Horizontal::Left) .into(), - widget::horizontal_space(Length::Fill).into(), + widget::horizontal_space().width(Length::Fill).into(), widget::text(key).into(), ]) .on_press(action.message()), @@ -294,13 +294,13 @@ where trees.push(MenuTree::::with_children( menu_button(vec![ widget::text(label).into(), - widget::horizontal_space(Length::Fill).into(), + widget::horizontal_space().width(Length::Fill).into(), widget::icon::from_name("pan-end-symbolic") .size(16) .icon() .into(), ]) - .style( + .class( // Menu folders have no on_press so they take on the disabled style by default if children.is_empty() { // This will make the folder use the disabled style if it has no children diff --git a/src/widget/min_size_tracker/mod.rs b/src/widget/min_size_tracker/mod.rs new file mode 100644 index 0000000..b190fec --- /dev/null +++ b/src/widget/min_size_tracker/mod.rs @@ -0,0 +1,319 @@ +mod subscription; + +use iced::futures::channel::mpsc::UnboundedSender; +use iced::widget::Container; +use iced::Vector; +pub use subscription::*; + +use iced_core::alignment; +use iced_core::event::{self, Event}; +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +use iced_core::renderer; +use iced_core::widget::Tree; +use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; +use std::{fmt::Debug, hash::Hash}; + +pub use iced_widget::container::{Catalog, Style}; + +pub fn min_size_tracker<'a, Message, I, T>( + content: T, + id: I, + tx: UnboundedSender<(I, Rectangle)>, +) -> MinSizeTrackerContainer<'a, Message, crate::Renderer, I> +where + I: Hash + Copy + Send + Sync + Debug + 'a, + T: Into>, +{ + MinSizeTrackerContainer::new(content, id, tx) +} + +pub fn subscription< + I: 'static + Hash + Copy + Send + Sync + Debug, + R: 'static + Hash + Copy + Send + Sync + Debug + Eq, +>( + id: I, +) -> iced::Subscription<(I, RectangleUpdate)> { + subscription::rectangle_tracker_subscription(id) +} + +#[derive(Clone, Debug)] +pub struct MinSizeTracker { + tx: UnboundedSender<(I, Rectangle)>, +} + +impl MinSizeTracker +where + I: Hash + Copy + Send + Sync + Debug, +{ + pub fn container<'a, Message: 'static, T>( + &self, + id: I, + content: T, + ) -> MinSizeTracker<'a, Message, crate::Renderer, I> + where + I: 'a, + T: Into>, + { + MinSizeTracker::new(content, id, self.tx.clone()) + } +} + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct MinSizeTrackerContainer<'a, Message, Renderer, I> +where + Renderer: iced_core::Renderer, +{ + tx: UnboundedSender<(I, Rectangle)>, + id: I, + container: Container<'a, Message, crate::Theme, Renderer>, + ignore_bounds: bool, +} + +impl<'a, Message, Renderer, I> MinSizeTrackerContainer<'a, Message, Renderer, I> +where + Renderer: iced_core::Renderer, + I: 'a + Hash + Copy + Send + Sync + Debug, +{ + /// Creates an empty [`Container`]. + pub(crate) fn new(content: T, id: I, tx: UnboundedSender<(I, Rectangle)>) -> Self + where + T: Into>, + { + MinSizeTrackerContainer { + id, + tx, + container: Container::new(content), + ignore_bounds: false, + } + } + + 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 { + self.container = self.container.padding(padding); + self + } + + /// Sets the width of the [`self.`]. + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.container = self.container.width(width); + self + } + + /// Sets the height of the [`Container`]. + #[must_use] + pub fn height(mut self, height: Length) -> Self { + self.container = self.container.height(height); + self + } + + /// Sets the maximum width of the [`Container`]. + #[must_use] + pub fn max_width(mut self, max_width: f32) -> Self { + self.container = self.container.max_width(max_width); + self + } + + /// Sets the maximum height of the [`Container`] in pixels. + #[must_use] + pub fn max_height(mut self, max_height: f32) -> Self { + self.container = self.container.max_height(max_height); + self + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + #[must_use] + pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { + self.container = self.container.align_x(alignment); + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + #[must_use] + pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { + self.container = self.container.align_y(alignment); + self + } + + /// Centers the contents in the horizontal axis of the [`Container`]. + #[must_use] + pub fn center_x(mut self, width: Length) -> Self { + self.container = self.container.center_x(width); + self + } + + /// Centers the contents in the vertical axis of the [`Container`]. + #[must_use] + pub fn center_y(mut self, height: Length) -> Self { + self.container = self.container.center_y(height); + self + } + + /// Sets the style of the [`Container`]. + #[must_use] + pub fn style(mut self, style: impl Into<::Class<'a>>) -> Self { + self.container = self.container.class(style); + self + } + + /// Set to true to ignore parent container bounds when performing layout. + /// This can be useful for widgets that are in auto-sized surfaces. + #[must_use] + pub fn ignore_bounds(mut self, ignore_bounds: bool) -> Self { + self.ignore_bounds = ignore_bounds; + self + } +} + +impl<'a, Message, Renderer, I> Widget + for MinSizeTrackerContainer<'a, Message, Renderer, I> +where + Renderer: iced_core::Renderer, + I: 'a + Hash + Copy + Send + Sync + Debug, +{ + fn children(&self) -> Vec { + 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); + } + + fn size(&self) -> iced_core::Size { + self.container.size() + } + + 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 + } else { + limits + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation<()>, + ) { + self.container.operate(tree, layout, renderer, operation); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &iced_core::Rectangle, + ) -> event::Status { + self.container.on_event( + tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + viewport, + ) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.container + .mouse_interaction(tree, layout, cursor_position, viewport, renderer) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let _ = self.tx.unbounded_send((self.id, layout.bounds())); + + self.container.draw( + tree, + renderer, + theme, + renderer_style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + self.container.overlay(tree, layout, renderer, translation) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + self.container + .drag_destinations(state, layout, renderer, dnd_rectangles); + } +} + +impl<'a, Message, Renderer, I> From> + for Element<'a, Message, crate::Theme, Renderer> +where + Message: 'a, + Renderer: 'a + iced_core::Renderer, + I: 'a + Hash + Copy + Send + Sync + Debug, +{ + fn from( + column: MinSizeTrackerContainer<'a, Message, Renderer, I>, + ) -> Element<'a, Message, crate::Theme, Renderer> { + Element::new(column) + } +} diff --git a/src/widget/min_size_tracker/subscription.rs b/src/widget/min_size_tracker/subscription.rs new file mode 100644 index 0000000..c7711b4 --- /dev/null +++ b/src/widget/min_size_tracker/subscription.rs @@ -0,0 +1,88 @@ +use iced::{ + futures::{ + channel::mpsc::{unbounded, UnboundedReceiver}, + stream, StreamExt, + }, + Rectangle, +}; +use iced_futures::Subscription; +use std::{collections::HashMap, fmt::Debug, hash::Hash}; + +use super::MinSizeTrackerContainer; + +pub fn rectangle_tracker_subscription< + I: 'static + Hash + Copy + Send + Sync + Debug, + R: 'static + Hash + Copy + Send + Sync + Debug + Eq, +>( + id: I, +) -> Subscription<(I, RectangleUpdate)> { + Subscription::run_with_id( + id, + stream::unfold(State::Ready, move |state| start_listening(id, state)), + ) +} + +pub enum State { + Ready, + Waiting(UnboundedReceiver<(I, Rectangle)>, HashMap), + Finished, +} + +async fn start_listening( + id: I, + mut state: State, +) -> Option<((I, RectangleUpdate), State)> { + loop { + let (update, new_state) = match state { + State::Ready => { + let (tx, rx) = unbounded(); + + ( + Some((id, RectangleUpdate::Init(MinSizeTracker { tx }))), + State::Waiting(rx, HashMap::new()), + ) + } + State::Waiting(mut rx, mut map) => match rx.next().await { + Some(u) => { + if let Some(prev) = map.get(&u.0) { + let new = u.1; + if (prev.width - new.width).abs() > 0.1 + || (prev.height - new.height).abs() > 0.1 + || (prev.x - new.x).abs() > 0.1 + || (prev.y - new.y).abs() > 0.1 + { + map.insert(u.0, new); + ( + Some((id, RectangleUpdate::Rectangle(u))), + State::Waiting(rx, map), + ) + } else { + (None, State::Waiting(rx, map)) + } + } else { + map.insert(u.0, u.1); + ( + Some((id, RectangleUpdate::Rectangle(u))), + State::Waiting(rx, map), + ) + } + } + None => (None, State::Finished), + }, + State::Finished => return None, + }; + state = new_state; + if let Some(u) = update { + return Some((u, state)); + } + } +} + +#[derive(Clone, Debug)] +pub enum RectangleUpdate +where + I: 'static + Hash + Copy + Send + Sync + Debug, +{ + Rectangle((I, Rectangle)), + Init(MinSizeTrackerContainer), +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 60bfcc3..ecce1a8 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -60,7 +60,7 @@ pub use iced::widget::{combo_box, ComboBox}; pub use iced::widget::{container, Container}; #[doc(inline)] -pub use iced::widget::{horizontal_space, space, vertical_space, Space}; +pub use iced::widget::{horizontal_space, vertical_space, Space}; #[doc(inline)] pub use iced::widget::{image, Image}; @@ -94,6 +94,9 @@ pub use iced_core::widget::{Id, Operation, Widget}; pub mod aspect_ratio; +#[cfg(feature = "autosize")] +pub mod autosize; + pub mod button; #[doc(inline)] pub use button::{Button, IconButton, LinkButton, TextButton}; @@ -166,20 +169,20 @@ pub mod divider { /// Horizontal divider with default thickness #[must_use] - pub fn default() -> Rule { - horizontal_rule(1).style(crate::theme::Rule::Default) + pub fn default<'a>() -> Rule<'a, crate::Theme> { + horizontal_rule(1).class(crate::theme::Rule::Default) } /// Horizontal divider with light thickness #[must_use] - pub fn light() -> Rule { - horizontal_rule(1).style(crate::theme::Rule::LightDivider) + pub fn light<'a>() -> Rule<'a, crate::Theme> { + horizontal_rule(1).class(crate::theme::Rule::LightDivider) } /// Horizontal divider with heavy thickness. #[must_use] - pub fn heavy() -> Rule { - horizontal_rule(4).style(crate::theme::Rule::HeavyDivider) + pub fn heavy<'a>() -> Rule<'a, crate::Theme> { + horizontal_rule(4).class(crate::theme::Rule::HeavyDivider) } } @@ -189,20 +192,20 @@ pub mod divider { /// Vertical divider with default thickness #[must_use] - pub fn default() -> Rule { - vertical_rule(1).style(crate::theme::Rule::Default) + pub fn default<'a>() -> Rule<'a, crate::Theme> { + vertical_rule(1).class(crate::theme::Rule::Default) } /// Vertical divider with light thickness #[must_use] - pub fn light() -> Rule { - vertical_rule(4).style(crate::theme::Rule::LightDivider) + pub fn light<'a>() -> Rule<'a, crate::Theme> { + vertical_rule(4).class(crate::theme::Rule::LightDivider) } /// Vertical divider with heavy thickness. #[must_use] - pub fn heavy() -> Rule { - vertical_rule(10).style(crate::theme::Rule::HeavyDivider) + pub fn heavy<'a>() -> Rule<'a, crate::Theme> { + vertical_rule(10).class(crate::theme::Rule::HeavyDivider) } } } @@ -268,7 +271,7 @@ pub use radio::{radio, Radio}; pub mod rectangle_tracker; #[doc(inline)] -pub use rectangle_tracker::{rectangle_tracker, RectangleTracker}; +pub use rectangle_tracker::{rectangle_tracking_container, RectangleTracker}; #[doc(inline)] pub use row::{row, Row}; @@ -343,13 +346,13 @@ pub mod tooltip { pub fn tooltip<'a, Message>( content: impl Into>, - tooltip: impl Into>, + tooltip: impl Into>, position: Position, ) -> Tooltip<'a, Message> { let xxs = crate::theme::active().cosmic().space_xxs(); Tooltip::new(content, tooltip, position) - .style(crate::theme::Container::Tooltip) + .class(crate::theme::Container::Tooltip) .padding(xxs) .gap(1) } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 246293c..cc62d14 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -147,11 +147,12 @@ impl<'a, Message: Clone + 'static> From> .spacing(space_xxs) .style(crate::theme::SegmentedButton::TabBar) .apply(scrollable) + .class(crate::style::iced::Scrollable::Minimal) .height(Length::Fill) .apply(container) .padding(space_xxs) .height(Length::Fill) - .style(theme::Container::custom(nav_bar_style)) + .class(theme::Container::custom(nav_bar_style)) } } @@ -162,9 +163,9 @@ impl<'a, Message: Clone + 'static> From> for crate::Element< } #[must_use] -pub fn nav_bar_style(theme: &Theme) -> iced_style::container::Appearance { +pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { let cosmic = &theme.cosmic(); - iced_style::container::Appearance { + iced_widget::container::Style { icon_color: Some(cosmic.on_bg_color().into()), text_color: Some(cosmic.on_bg_color().into()), background: Some(Background::Color(cosmic.primary.base.into())), diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 68dd528..bd14cf3 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -11,7 +11,7 @@ pub struct NavBarToggle { active: bool, #[setters(strip_option)] on_toggle: Option, - style: crate::theme::Button, + class: crate::theme::Button, selected: bool, } @@ -20,7 +20,7 @@ pub fn nav_bar_toggle() -> NavBarToggle { NavBarToggle { active: false, on_toggle: None, - style: crate::theme::Button::Text, + class: crate::theme::Button::Text, selected: false, } } @@ -43,7 +43,7 @@ impl<'a, Message: 'static + Clone> From> for Element<'a, M .padding([8, 16]) .on_press_maybe(nav_bar_toggle.on_toggle) .selected(nav_bar_toggle.selected) - .style(nav_bar_toggle.style) + .class(nav_bar_toggle.class) .into() } } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index cb15da9..4eb50ea 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -9,12 +9,12 @@ use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; use iced_core::touch; -use iced_core::widget::{tree, Operation, OperationOutputWrapper, Tree}; +use iced_core::widget::{tree, Operation, Tree}; use iced_core::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; -pub use iced_style::container::{Appearance, StyleSheet}; +pub use iced_widget::container::{Catalog, Style}; pub fn popover<'a, Message, Renderer>( content: impl Into>, @@ -123,7 +123,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { self.content .as_widget() @@ -214,6 +214,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + mut translation: Vector, ) -> Option> { if !tree.state.downcast_mut::().is_open { return None; @@ -239,19 +240,22 @@ where // Round position to prevent rendering issues overlay_position.x = overlay_position.x.round(); overlay_position.y = overlay_position.y.round(); + translation.x += overlay_position.x; + translation.y += overlay_position.y; - Some(overlay::Element::new( - overlay_position, - Box::new(Overlay { - tree: &mut tree.children[1], - content: popup, - position: self.position, - }), - )) + Some(overlay::Element::new(Box::new(Overlay { + tree: &mut tree.children[1], + content: popup, + position: self.position, + pos: Point::new(translation.x, translation.y), + }))) } else { - self.content - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer) + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + translation, + ) } } @@ -286,6 +290,7 @@ pub struct Overlay<'a, 'b, Message, Renderer> { tree: &'a mut Tree, content: &'a mut Element<'b, Message, crate::Theme, Renderer>, position: Position, + pos: Point, } impl<'a, 'b, Message, Renderer> overlay::Overlay @@ -294,13 +299,8 @@ where Message: Clone, Renderer: iced_core::Renderer, { - fn layout( - &mut self, - renderer: &Renderer, - bounds: Size, - mut position: Point, - _translation: iced::Vector, - ) -> layout::Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { + let mut position = self.pos; let limits = layout::Limits::new(Size::UNIT, bounds); let node = self .content @@ -342,7 +342,7 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { self.content .as_widget() @@ -413,7 +413,7 @@ where ) -> Option> { self.content .as_widget_mut() - .overlay(&mut self.tree, layout, renderer) + .overlay(&mut self.tree, layout, renderer, Default::default()) } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index c509b49..1b40a0e 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,4 +1,6 @@ //! Create choices using radio buttons. +use crate::Theme; +use iced::border; use iced_core::event::{self, Event}; use iced_core::layout; use iced_core::mouse; @@ -7,17 +9,18 @@ use iced_core::renderer; use iced_core::touch; use iced_core::widget::tree::Tree; use iced_core::{ - Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, + Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, }; -pub use iced_style::radio::{Appearance, StyleSheet}; +use iced_widget::radio as iced_radio; +pub use iced_widget::radio::Catalog; -pub fn radio<'a, Message: Clone, Theme: StyleSheet, V, F>( +pub fn radio<'a, Message: Clone, V, F>( label: impl Into>, value: V, selected: Option, f: F, -) -> Radio<'a, Message, Theme, crate::Renderer> +) -> Radio<'a, Message, crate::Renderer> where V: Eq + Copy, F: FnOnce(V) -> Message, @@ -83,9 +86,8 @@ where /// let content = column![a, b, c, all]; /// ``` #[allow(missing_debug_implementations)] -pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +pub struct Radio<'a, Message, Renderer = crate::Renderer> where - Theme: StyleSheet, Renderer: iced_core::Renderer, { is_selected: bool, @@ -94,13 +96,11 @@ where width: Length, size: f32, spacing: f32, - style: Theme::Style, } -impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> +impl<'a, Message, Renderer> Radio<'a, Message, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: iced_core::Renderer, { /// The default size of a [`Radio`] button. @@ -130,7 +130,6 @@ where width: Length::Shrink, size: Self::DEFAULT_SIZE, spacing: Self::DEFAULT_SPACING, - style: Default::default(), } } @@ -154,20 +153,11 @@ where self.spacing = spacing.into().0; self } - - #[must_use] - /// Sets the style of the [`Radio`] button. - pub fn style(mut self, style: impl Into) -> Self { - self.style = style.into(); - self - } } -impl<'a, Message, Theme, Renderer> Widget - for Radio<'a, Message, Theme, Renderer> +impl<'a, Message, Renderer> Widget for Radio<'a, Message, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: iced_core::Renderer, { fn children(&self) -> Vec { @@ -207,9 +197,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { self.label.as_widget().operate( &mut tree.children[0], @@ -302,9 +290,19 @@ where let mut children = layout.children(); let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_selected) + theme.style( + &(), + iced_radio::Status::Hovered { + is_selected: self.is_selected, + }, + ) } else { - theme.active(&self.style, self.is_selected) + theme.style( + &(), + iced_radio::Status::Active { + is_selected: self.is_selected, + }, + ) }; { @@ -336,7 +334,7 @@ where width: dot_size, height: dot_size, }, - border: Border::with_radius(dot_size / 2.0), + border: border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, custom_style.dot_color, @@ -363,11 +361,13 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, + translation, ) } @@ -376,7 +376,7 @@ where state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.label.as_widget().drag_destinations( &state.children[0], @@ -387,14 +387,13 @@ where } } -impl<'a, Message, Theme, Renderer> From> +impl<'a, Message, Renderer> From> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: 'a + StyleSheet, Renderer: 'a + iced_core::Renderer, { - fn from(radio: Radio<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> { + fn from(radio: Radio<'a, Message, Renderer>) -> Element<'a, Message, Theme, Renderer> { Element::new(radio) } } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 93b861a..6ce1139 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -2,6 +2,7 @@ mod subscription; use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; +use iced::Vector; pub use subscription::*; use iced_core::alignment; @@ -14,9 +15,9 @@ use iced_core::widget::Tree; use iced_core::{Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; use std::{fmt::Debug, hash::Hash}; -pub use iced_style::container::{Appearance, StyleSheet}; +pub use iced_widget::container::{Catalog, Style}; -pub fn rectangle_tracker<'a, Message, I, T>( +pub fn rectangle_tracking_container<'a, Message, I, T>( content: T, id: I, tx: UnboundedSender<(I, Rectangle)>, @@ -71,6 +72,7 @@ where id: I, container: Container<'a, Message, crate::Theme, Renderer>, ignore_bounds: bool, + request_size: bool, } impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I> @@ -88,6 +90,7 @@ where tx, container: Container::new(content), ignore_bounds: false, + request_size: true, } } @@ -146,22 +149,22 @@ where /// Centers the contents in the horizontal axis of the [`Container`]. #[must_use] - pub fn center_x(mut self) -> Self { - self.container = self.container.center_x(); + pub fn center_x(mut self, width: Length) -> Self { + self.container = self.container.center_x(width); self } /// Centers the contents in the vertical axis of the [`Container`]. #[must_use] - pub fn center_y(mut self) -> Self { - self.container = self.container.center_y(); + pub fn center_y(mut self, height: Length) -> Self { + self.container = self.container.center_y(height); self } /// Sets the style of the [`Container`]. #[must_use] - pub fn style(mut self, style: impl Into<::Style>) -> Self { - self.container = self.container.style(style); + pub fn style(mut self, style: impl Into<::Class<'a>>) -> Self { + self.container = self.container.class(style); self } @@ -202,7 +205,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.container.layout( + let layout = self.container.layout( tree, renderer, if self.ignore_bounds { @@ -210,7 +213,10 @@ where } else { limits }, - ) + ); + if self.request_size {} + + layout } fn operate( @@ -218,9 +224,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container.operate(tree, layout, renderer, operation); } @@ -271,7 +275,6 @@ where viewport: &Rectangle, ) { let _ = self.tx.unbounded_send((self.id, layout.bounds())); - self.container.draw( tree, renderer, @@ -288,8 +291,9 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer) + self.container.overlay(tree, layout, renderer, translation) } fn drag_destinations( @@ -297,7 +301,7 @@ where state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.container .drag_destinations(state, layout, renderer, dnd_rectangles); diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 224f8d6..e49946d 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -1,10 +1,11 @@ use iced::{ futures::{ channel::mpsc::{unbounded, UnboundedReceiver}, - StreamExt, + stream, StreamExt, }, - subscription, Rectangle, + Rectangle, }; +use iced_futures::Subscription; use std::{collections::HashMap, fmt::Debug, hash::Hash}; use super::RectangleTracker; @@ -14,8 +15,11 @@ pub fn rectangle_tracker_subscription< R: 'static + Hash + Copy + Send + Sync + Debug + Eq, >( id: I, -) -> iced::Subscription<(I, RectangleUpdate)> { - subscription::unfold(id, State::Ready, move |state| start_listening(id, state)) +) -> Subscription<(I, RectangleUpdate)> { + Subscription::run_with_id( + id, + stream::unfold(State::Ready, move |state| start_listening(id, state)), + ) } pub enum State { @@ -27,7 +31,7 @@ pub enum State { async fn start_listening( id: I, mut state: State, -) -> ((I, RectangleUpdate), State) { +) -> Option<((I, RectangleUpdate), State)> { loop { let (update, new_state) = match state { State::Ready => { @@ -65,11 +69,11 @@ async fn start_listening (None, State::Finished), }, - State::Finished => iced::futures::future::pending().await, + State::Finished => return None, }; state = new_state; if let Some(u) = update { - return (u, state); + return Some((u, state)); } } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index f070c7e..9fa62d4 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -16,14 +16,15 @@ use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, O use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ - alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Command, Event, Length, - Padding, Rectangle, Size, + alignment, event, keyboard, mouse, touch, Alignment, Background, Color, Event, Length, Padding, + Rectangle, Size, Task, Vector, }; use iced_core::mouse::ScrollDelta; -use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping, Wrap}; +use iced_core::text::{LineHeight, Paragraph, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::{self, operation, tree}; use iced_core::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; use iced_core::{Border, Gradient, Point, Renderer as IcedRenderer, Shadow, Text}; +use iced_runtime::{task, Action}; use slotmap::{Key, SecondaryMap}; use std::borrow::Cow; use std::collections::hash_map::DefaultHasher; @@ -34,8 +35,8 @@ use std::mem; use std::time::{Duration, Instant}; /// A command that focuses a segmented item stored in a widget. -pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id.0)) +pub fn focus(id: Id) -> Task { + task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0)))) } pub enum ItemBounds { @@ -436,15 +437,15 @@ where if !text.is_empty() { icon_spacing = f32::from(self.button_spacing); let paragraph = entry.or_insert_with(|| { - crate::Paragraph::with_text(Text { - content: text, + crate::Plain::new(Text { + content: text.as_ref(), size: iced::Pixels(self.font_size), bounds: Size::INFINITY, font, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, - wrap: Wrap::default(), + wrapping: Wrapping::default(), line_height: self.line_height, }) }); @@ -622,23 +623,21 @@ where } let text = Text { - content: text, + content: text.as_ref(), size: iced::Pixels(self.font_size), bounds: Size::INFINITY, font, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, - wrap: Wrap::default(), + wrapping: Wrapping::default(), line_height: self.line_height, }; if let Some(paragraph) = state.paragraphs.get_mut(key) { paragraph.update(text); } else { - state - .paragraphs - .insert(key, crate::Paragraph::with_text(text)); + state.paragraphs.insert(key, crate::Plain::new(text)); } } } @@ -1079,9 +1078,7 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation< - iced_core::widget::OperationOutputWrapper, - >, + operation: &mut dyn iced_core::widget::Operation<()>, ) { let state = tree.state.downcast_mut::(); operation.focusable(state, Some(&self.id.0)); @@ -1492,7 +1489,7 @@ where if self.model.text(key).is_some_and(|text| !text.is_empty()) { // Draw the text for this segmented button or tab. renderer.fill_paragraph( - &state.paragraphs[key], + state.paragraphs[key].raw(), bounds.position(), apply_alpha(status_appearance.text_color), Rectangle { @@ -1528,6 +1525,7 @@ where tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &Renderer, + translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); @@ -1575,6 +1573,7 @@ where root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), style: &crate::theme::menu_bar::MenuBarStyle::Default, + position: Point::new(translation.x, translation.y), } .overlay(), ) @@ -1645,7 +1644,7 @@ pub struct LocalState { /// Dimensions of internal buttons when shrinking pub(super) internal_layout: Vec<(Size, Size)>, /// The paragraphs for each text. - paragraphs: SecondaryMap, + paragraphs: SecondaryMap, /// Used to detect changes in text. text_hashes: SecondaryMap, /// Location of cursor when context menu was opened. diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index b21df31..f277ff0 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -9,7 +9,7 @@ use crate::{ Element, }; use derive_setters::Setters; -use iced_core::{text::Wrap, Length}; +use iced_core::{text::Wrapping, Length}; use taffy::AlignContent; /// A settings item aligned in a row @@ -20,8 +20,8 @@ pub fn item<'a, Message: 'static>( widget: impl Into> + 'a, ) -> Row<'a, Message> { item_row(vec![ - text(title).wrap(Wrap::Word).into(), - horizontal_space(iced::Length::Fill).into(), + text(title).wrapping(Wrapping::Word).into(), + horizontal_space().width(iced::Length::Fill).into(), widget.into(), ]) } @@ -35,7 +35,7 @@ pub fn item_row(children: Vec>) -> Row { } = theme::THEME.lock().unwrap().cosmic().spacing; row::with_children(children) .spacing(space_xs) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .padding([0, space_s]) } @@ -46,7 +46,10 @@ pub fn flex_item<'a, Message: 'static>( widget: impl Into> + 'a, ) -> FlexRow<'a, Message> { flex_item_row(vec![ - text(title).wrap(Wrap::Word).width(Length::Fill).into(), + text(title) + .wrapping(Wrapping::Word) + .width(Length::Fill) + .into(), container(widget).into(), ]) } @@ -111,8 +114,8 @@ impl<'a, Message: 'static> Item<'a, Message> { if let Some(description) = self.description { let column = column::with_capacity(2) .spacing(2) - .push(text(self.title).wrap(Wrap::Word)) - .push(text(description).wrap(Wrap::Word).size(10)) + .push(text(self.title).wrapping(Wrapping::Word)) + .push(text(description).wrapping(Wrapping::Word).size(10)) .width(Length::Fill); contents.push(column.into()); @@ -129,6 +132,6 @@ impl<'a, Message: 'static> Item<'a, Message> { is_checked: bool, message: impl Fn(bool) -> Message + 'static, ) -> Row<'a, Message> { - self.control(crate::widget::toggler(None, is_checked, message)) + self.control(crate::widget::toggler(is_checked, message)) } } diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs index d0fbbca..7ed3e53 100644 --- a/src/widget/spin_button/mod.rs +++ b/src/widget/spin_button/mod.rs @@ -56,11 +56,10 @@ impl<'a, Message: 'static> SpinButton<'a, Message> { .apply(button::custom) .width(Length::Fixed(32.0)) .height(Length::Fixed(32.0)) - .style(theme::Button::Text) + .class(theme::Button::Text) .on_press(model::Message::Decrement) .into(), text::title4(label) - .vertical_alignment(Vertical::Center) .apply(container) .width(Length::Fixed(48.0)) .align_x(Horizontal::Center) @@ -76,18 +75,18 @@ impl<'a, Message: 'static> SpinButton<'a, Message> { .apply(button::custom) .width(Length::Fixed(32.0)) .height(Length::Fixed(32.0)) - .style(theme::Button::Text) + .class(theme::Button::Text) .on_press(model::Message::Increment) .into(), ]) .width(Length::Shrink) .height(Length::Fixed(32.0)) - .align_items(Alignment::Center), + .align_y(Alignment::Center), ) .align_y(Vertical::Center) .width(Length::Shrink) .height(Length::Fixed(32.0)) - .style(theme::Container::custom(container_style)) + .class(theme::Container::custom(container_style)) .apply(Element::from) .map(on_change) } @@ -100,13 +99,13 @@ impl<'a, Message: 'static> From> for Element<'a, Message } #[allow(clippy::trivially_copy_pass_by_ref)] -fn container_style(theme: &crate::Theme) -> iced_style::container::Appearance { +fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { let basic = &theme.cosmic(); let mut neutral_10 = basic.palette.neutral_10; neutral_10.alpha = 0.1; let accent = &basic.accent; let corners = &basic.corner_radii; - iced_style::container::Appearance { + iced_widget::container::Style { icon_color: Some(basic.palette.neutral_10.into()), text_color: Some(basic.palette.neutral_10.into()), background: None, diff --git a/src/widget/text.rs b/src/widget/text.rs index 1aed950..f1e3cff 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; /// /// [`Text`]: widget::Text pub fn text<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text).font(crate::font::default()) + Text::new(text.into()).font(crate::font::default()) } /// Available presets for text typography @@ -26,7 +26,7 @@ pub enum Typography { /// [`Text`] widget with the Title 1 typography preset. pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(32.0) .line_height(LineHeight::Absolute(44.0.into())) .font(crate::font::semibold()) @@ -34,7 +34,7 @@ pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, /// [`Text`] widget with the Title 2 typography preset. pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(28.0) .line_height(LineHeight::Absolute(36.0.into())) .font(crate::font::default()) @@ -42,7 +42,7 @@ pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, /// [`Text`] widget with the Title 3 typography preset. pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(24.0) .line_height(LineHeight::Absolute(32.0.into())) .font(crate::font::default()) @@ -50,7 +50,7 @@ pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, /// [`Text`] widget with the Title 4 typography preset. pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(20.0) .line_height(LineHeight::Absolute(28.0.into())) .font(crate::font::default()) @@ -58,7 +58,7 @@ pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, /// [`Text`] widget with the Heading typography preset. pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(14.0) .line_height(LineHeight::Absolute(iced::Pixels(20.0))) .font(crate::font::semibold()) @@ -66,7 +66,7 @@ pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, /// [`Text`] widget with the Caption Heading typography preset. pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(10.0) .line_height(LineHeight::Absolute(iced::Pixels(14.0))) .font(crate::font::semibold()) @@ -74,7 +74,7 @@ pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, crate /// [`Text`] widget with the Body typography preset. pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(14.0) .line_height(LineHeight::Absolute(20.0.into())) .font(crate::font::default()) @@ -82,7 +82,7 @@ pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Re /// [`Text`] widget with the Caption typography preset. pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(10.0) .line_height(LineHeight::Absolute(14.0.into())) .font(crate::font::default()) @@ -90,7 +90,7 @@ pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, /// [`Text`] widget with the Monotext typography preset. pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text) + Text::new(text.into()) .size(14.0) .line_height(LineHeight::Absolute(20.0.into())) .font(crate::font::mono()) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 90a0f57..3c08562 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -18,6 +18,9 @@ use super::style::StyleSheet; pub use super::value::Value; use apply::Apply; +use cosmic_theme::Theme; +use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; +use iced::clipboard::mime::AsMimeTypes; use iced::Limits; use iced_core::event::{self, Event}; use iced_core::mouse::{self, click}; @@ -39,15 +42,9 @@ use iced_core::{ }; #[cfg(feature = "wayland")] use iced_renderer::core::event::{wayland, PlatformSpecific}; -use iced_renderer::core::widget::OperationOutputWrapper; #[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific; -use iced_runtime::Command; - -#[cfg(feature = "wayland")] -use cctk::sctk::reexports::client::protocol::wl_data_device_manager::DndAction; -#[cfg(feature = "wayland")] -use iced_runtime::command::platform_specific::wayland::data_device::{DataFromMimeType, DndIcon}; +use iced_runtime::platform_specific; +use iced_runtime::{task, Action, Task}; thread_local! { // Prevents two inputs from being focused at the same time. @@ -146,7 +143,7 @@ where }) .size(16) .apply(crate::widget::button::custom) - .style(crate::theme::Button::Icon) + .class(crate::theme::Button::Icon) .on_press(msg) .padding(8) .into(), @@ -173,7 +170,6 @@ where .padding(spacing) } -#[cfg(feature = "wayland")] pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[ "text/plain;charset=utf-8", "text/plain;charset=UTF-8", @@ -182,17 +178,12 @@ pub(crate) const SUPPORTED_TEXT_MIME_TYPES: &[&str; 6] = &[ "text/plain", "TEXT", ]; -#[cfg(feature = "wayland")] -pub type DnDCommand = - Box platform_specific::wayland::data_device::ActionInner>; -#[cfg(not(feature = "wayland"))] -pub type DnDCommand = (); /// A field that can be filled with text. #[allow(missing_debug_implementations)] #[must_use] pub struct TextInput<'a, Message> { - id: Option, + id: Id, placeholder: Cow<'a, str>, value: Value, is_secure: bool, @@ -215,7 +206,6 @@ pub struct TextInput<'a, Message> { trailing_icon: Option>, style: ::Style, on_create_dnd_source: Option Message + 'a>>, - on_dnd_command_produced: Option Message + 'a>>, surface_ids: Option<(window::Id, window::Id)>, dnd_icon: bool, line_height: text::LineHeight, @@ -237,7 +227,7 @@ where let v: Cow<'a, str> = value.into(); TextInput { - id: None, + id: Id::unique(), placeholder: placeholder.into(), value: Value::new(v.as_ref()), is_secure: false, @@ -258,7 +248,6 @@ where trailing_icon: None, error: None, style: crate::theme::TextInput::default(), - on_dnd_command_produced: None, on_create_dnd_source: None, surface_ids: None, dnd_icon: false, @@ -269,6 +258,15 @@ where } } + fn dnd_id(&self) -> u128 { + match &self.id.0 { + iced_core::id::Internal::Custom(id, _) | iced_core::id::Internal::Unique(id) => { + *id as u128 + } + _ => unreachable!(), + } + } + /// Sets the input to be always active. /// This makes it behave as if it was always focused. pub fn always_active(mut self) -> Self { @@ -290,7 +288,7 @@ where /// Sets the [`Id`] of the [`TextInput`]. pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + self.id = id; self } @@ -433,10 +431,12 @@ where value: Option<&Value>, style: &renderer::Style, ) { + let text_layout = self.text_layout(layout.clone()); draw( renderer, theme, layout, + text_layout, cursor_position, tree, value.unwrap_or(&self.value), @@ -467,20 +467,6 @@ where self } - /// Sets the dnd command produced handler of the [`TextInput`]. - /// Commands should be returned in the update function of the application. - #[cfg(feature = "wayland")] - pub fn on_dnd_command_produced( - mut self, - on_dnd_command_produced: impl Fn( - Box platform_specific::wayland::data_device::ActionInner>, - ) -> Message - + 'a, - ) -> Self { - self.on_dnd_command_produced = Some(Box::new(on_dnd_command_produced)); - self - } - /// Sets the window id of the [`TextInput`] and the window id of the drag icon. /// Both ids are required to be unique. /// This is required for the dnd to work. @@ -500,7 +486,7 @@ where crate::widget::icon::from_name("edit-clear-symbolic") .size(16) .apply(crate::widget::button::custom) - .style(crate::theme::Button::Icon) + .class(crate::theme::Button::Icon) .on_press(on_clear) .padding(8) .into(), @@ -550,6 +536,7 @@ where } let old_value = state .value + .raw() .buffer() .lines .iter() @@ -559,6 +546,7 @@ where || old_value != self.value.to_string() || state .label + .raw() .buffer() .lines .iter() @@ -567,6 +555,7 @@ where != self.label.unwrap_or_default() || state .helper_text + .raw() .buffer() .lines .iter() @@ -651,7 +640,7 @@ where let v = self.value.to_string(); value_paragraph.update(Text { content: if self.value.is_empty() { - &self.placeholder + self.placeholder.as_ref() } else { &v }, @@ -662,7 +651,7 @@ where vertical_alignment: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); let Size { width, height } = @@ -717,12 +706,13 @@ where tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { let state = tree.state.downcast_mut::(); - operation.focusable(state, self.id.as_ref()); - operation.text_input(state, self.id.as_ref()); + operation.custom(state, Some(&self.id)); + operation.focusable(state, Some(&self.id)); + operation.text_input(state, Some(&self.id)); } fn overlay<'b>( @@ -730,6 +720,7 @@ where tree: &'b mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, + translation: Vector, ) -> Option> { let mut layout_ = Vec::with_capacity(2); if self.leading_icon.is_some() { @@ -752,7 +743,9 @@ where .zip(&mut tree.children) .zip(layout_) .filter_map(|((child, state), layout)| { - child.as_widget_mut().overlay(state, layout, renderer) + child + .as_widget_mut() + .overlay(state, layout, renderer, translation) }) .collect::>(); @@ -814,8 +807,10 @@ where } } } - + let dnd_id = self.dnd_id(); + let id = Widget::id(self); update( + id, event, text_layout.children().next().unwrap(), trailing_icon_layout, @@ -833,9 +828,7 @@ where self.on_toggle_edit.as_deref(), || tree.state.downcast_mut::(), self.on_create_dnd_source.as_deref(), - self.dnd_icon, - self.on_dnd_command_produced.as_deref(), - self.surface_ids, + dnd_id, line_height, layout, ) @@ -851,10 +844,12 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { + let text_layout = self.text_layout(layout.clone()); draw( renderer, theme, layout, + text_layout, cursor_position, tree, &self.value, @@ -934,11 +929,43 @@ where } fn id(&self) -> Option { - self.id.clone() + Some(self.id.clone()) } fn set_id(&mut self, id: Id) { - self.id = Some(id); + self.id = id; + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &crate::Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + if let Some(input) = layout.children().last() { + let Rectangle { + x, + y, + width, + height, + } = input.bounds(); + dnd_rectangles.push(iced::clipboard::dnd::DndDestinationRectangle { + id: self.dnd_id(), + rectangle: iced::clipboard::dnd::Rectangle { + x: x as f64, + y: y as f64, + width: width as f64, + height: height as f64, + }, + mime_types: SUPPORTED_TEXT_MIME_TYPES + .iter() + .map(|s| Cow::Borrowed(*s)) + .collect(), + actions: DndAction::Move, + preferred: DndAction::Move, + }); + } } } @@ -954,32 +981,38 @@ where } } -/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus(id: Id) -> Command { - Command::widget(operation::focusable::focus(id)) +/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus(id: Id) -> Task { + task::effect(Action::widget(operation::focusable::focus(id))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. -pub fn move_cursor_to_end(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_end(id)) +pub fn move_cursor_to_end(id: Id) -> Task { + task::effect(Action::widget(operation::text_input::move_cursor_to_end( + id, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. -pub fn move_cursor_to_front(id: Id) -> Command { - Command::widget(operation::text_input::move_cursor_to_front(id)) +pub fn move_cursor_to_front(id: Id) -> Task { + task::effect(Action::widget(operation::text_input::move_cursor_to_front( + id, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. -pub fn move_cursor_to(id: Id, position: usize) -> Command { - Command::widget(operation::text_input::move_cursor_to(id, position)) +pub fn move_cursor_to(id: Id, position: usize) -> Task { + task::effect(Action::widget(operation::text_input::move_cursor_to( + id, position, + ))) } -/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all(id: Id) -> Command { - Command::widget(operation::text_input::select_all(id)) +/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. +pub fn select_all(id: Id) -> Task { + task::effect(Action::widget(operation::text_input::select_all(id))) } /// Computes the layout of a [`TextInput`]. @@ -1019,7 +1052,7 @@ pub fn layout( vertical_alignment: alignment::Vertical::Center, line_height, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); let label_size = label_paragraph.min_bounds(); @@ -1158,7 +1191,7 @@ pub fn layout( vertical_alignment: alignment::Vertical::Center, line_height: helper_text_line_height, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); let helper_text_size = helper_text_paragraph.min_bounds(); let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos); @@ -1188,7 +1221,8 @@ pub fn layout( #[allow(clippy::missing_panics_doc)] #[allow(clippy::cast_lossless)] #[allow(clippy::cast_possible_truncation)] -pub fn update<'a, Message>( +pub fn update<'a, Message: 'static>( + id: Option, event: Event, text_layout: Layout<'_>, trailing_icon_layout: Option>, @@ -1206,9 +1240,7 @@ pub fn update<'a, Message>( on_toggle_edit: Option<&dyn Fn(bool) -> Message>, state: impl FnOnce() -> &'a mut State, #[allow(unused_variables)] on_start_dnd_source: Option<&dyn Fn(State) -> Message>, - #[allow(unused_variables)] dnd_icon: bool, - #[allow(unused_variables)] on_dnd_command_produced: Option<&dyn Fn(DnDCommand) -> Message>, - #[allow(unused_variables)] surface_ids: Option<(window::Id, window::Id)>, + #[allow(unused_variables)] dnd_id: u128, line_height: text::LineHeight, layout: Layout<'_>, ) -> event::Status @@ -1253,7 +1285,8 @@ where let text_layout = layout.children().next().unwrap(); let target = cursor_position.x - text_layout.bounds().x; - let click = mouse::Click::new(cursor_position, state.last_click); + let click = + mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); match ( &state.dragging_state, @@ -1266,28 +1299,18 @@ where // single click that is on top of the selected text // is the click on selected text? - if let ( - Some(on_start_dnd), - Some(on_dnd_command_produced), - Some((window_id, icon_id)), - Some(on_input), - ) = ( - on_start_dnd_source, - on_dnd_command_produced, - surface_ids, - on_input, - ) { + if let Some(on_input) = on_input { let left = start.min(end); let right = end.max(start); let (left_position, _left_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_layout.bounds(), left, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_layout.bounds(), right, ); @@ -1305,10 +1328,12 @@ where if is_secure { return event::Status::Ignored; } - let text = + let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); - state.dragging_state = - Some(DraggingState::Dnd(DndAction::empty(), text.clone())); + state.dragging_state = Some(DraggingState::Dnd( + DndAction::empty(), + input_text.clone(), + )); let mut editor = Editor::new(unsecured_value, &mut state.cursor); editor.delete(); @@ -1316,23 +1341,25 @@ where let unsecured_value = Value::new(&contents); let message = (on_input)(contents); shell.publish(message); - shell.publish(on_start_dnd(state.clone())); + if let Some(on_start_dnd) = on_start_dnd_source { + shell.publish(on_start_dnd(state.clone())); + } let state_clone = state.clone(); - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::StartDnd { - mime_types: SUPPORTED_TEXT_MIME_TYPES - .iter() - .map(std::string::ToString::to_string) - .collect(), - actions: DndAction::Move, - origin_id: window_id, - icon_id: Some(( - DndIcon::Widget(icon_id, Box::new(state_clone.clone())), - iced::Vector::ZERO, - )), - data: Box::new(TextInputString(text.clone())), - } - }))); + + iced_core::clipboard::start_dnd( + clipboard, + false, + id.map(|id| iced_core::clipboard::DndSource::Widget(id)), + Some(( + Element::from( + TextInput::<'static, ()>::new("", input_text.clone()) + .dnd_icon(true), + ), + iced_core::widget::tree::State::new(state_clone), + )), + Box::new(TextInputString(input_text)), + DndAction::Move, + ); update_cache(state, &unsecured_value); } else { @@ -1435,7 +1462,7 @@ where return event::Status::Ignored; }; - let click = mouse::Click::new(pos, state.last_click); + let click = mouse::Click::new(pos, mouse::Button::Left, state.last_click); match ( &state.dragging_state, @@ -1621,7 +1648,10 @@ where { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { - clipboard.write(value.select(start, end).to_string()); + clipboard.write( + iced_core::clipboard::Kind::Primary, + value.select(start, end).to_string(), + ); } } } @@ -1632,7 +1662,10 @@ where { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { - clipboard.write(value.select(start, end).to_string()); + clipboard.write( + iced_core::clipboard::Kind::Primary, + value.select(start, end).to_string(), + ); } let mut editor = Editor::new(value, &mut state.cursor); @@ -1650,7 +1683,7 @@ where content } else { let content: String = clipboard - .read() + .read(iced_core::clipboard::Kind::Primary) .unwrap_or_default() .chars() .filter(|c| !c.is_control()) @@ -1760,7 +1793,7 @@ where state.keyboard_modifiers = modifiers; } - Event::Window(_, window::Event::RedrawRequested(now)) => { + Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = &mut state.is_focused { @@ -1775,9 +1808,7 @@ where } } #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource( - wayland::DataSourceEvent::DndFinished | wayland::DataSourceEvent::Cancelled, - ))) => { + Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { let state = state(); if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { state.dragging_state = None; @@ -1785,54 +1816,32 @@ where } } #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DataSource( - wayland::DataSourceEvent::DndActionAccepted(action), - ))) => { - let state = state(); - if let Some(DraggingState::Dnd(_, text)) = state.dragging_state.as_ref() { - state.dragging_state = Some(DraggingState::Dnd(action, text.clone())); - return event::Status::Captured; - } - } - #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::Enter { x, y, mime_types }, - ))) => { - let Some(on_dnd_command_produced) = on_dnd_command_produced else { - return event::Status::Ignored; - }; - + Event::Dnd(DndEvent::Offer( + rectangle, + OfferEvent::Enter { + x, + y, + mime_types, + surface, + }, + )) if rectangle == Some(dnd_id) => { let state = state(); let is_clicked = text_layout.bounds().contains(Point { x: x as f32, y: y as f32, }); - if !is_clicked { - state.dnd_offer = DndOfferState::OutsideWidget(mime_types, DndAction::None); - return event::Status::Captured; - } let mut accepted = false; for m in &mime_types { if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { let clone = m.clone(); accepted = true; - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::Accept(Some( - clone.clone(), - )) - }))); } } if accepted { - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::SetActions { - preferred: DndAction::Move, - accepted: DndAction::Move.union(DndAction::Copy), - } - }))); let target = x as f32 - text_layout.bounds().x; - state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::None); + state.dnd_offer = + DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); // existing logic for setting the selection let position = if target > 0.0 { update_cache(state, value); @@ -1846,57 +1855,11 @@ where } } #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::Motion { x, y }, - ))) => { - let Some(on_dnd_command_produced) = on_dnd_command_produced else { - return event::Status::Ignored; - }; - + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) + if rectangle == Some(dnd_id) => + { let state = state(); - let is_clicked = text_layout.bounds().contains(Point { - x: x as f32, - y: y as f32, - }); - if !is_clicked { - if let DndOfferState::HandlingOffer(mime_types, action) = state.dnd_offer.clone() { - state.dnd_offer = DndOfferState::OutsideWidget(mime_types, action); - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::SetActions { - preferred: DndAction::None, - accepted: DndAction::None, - } - }))); - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::Accept(None) - }))); - } - return event::Status::Captured; - } else if let DndOfferState::OutsideWidget(mime_types, action) = state.dnd_offer.clone() - { - let mut accepted = false; - for m in &mime_types { - if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { - accepted = true; - let clone = m.clone(); - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::Accept(Some( - clone.clone(), - )) - }))); - } - } - if accepted { - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::SetActions { - preferred: DndAction::Move, - accepted: DndAction::Move.union(DndAction::Copy), - } - }))); - state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), action); - } - }; let target = x as f32 - text_layout.bounds().x; // existing logic for setting the selection let position = if target > 0.0 { @@ -1910,13 +1873,7 @@ where return event::Status::Captured; } #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::DropPerformed, - ))) => { - let Some(on_dnd_command_produced) = on_dnd_command_produced else { - return event::Status::Ignored; - }; - + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => { let state = state(); if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES @@ -1927,21 +1884,15 @@ where return event::Status::Captured; }; state.dnd_offer = DndOfferState::Dropped; - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::RequestDndData( - (*mime_type).to_string(), - ) - }))); - } else if let DndOfferState::OutsideWidget(..) = &state.dnd_offer { - state.dnd_offer = DndOfferState::None; - return event::Status::Captured; } + return event::Status::Ignored; } #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::Leave, - ))) => { + Event::Dnd(DndEvent::Offer( + rectangle, + OfferEvent::Leave | OfferEvent::LeaveDestination, + )) if rectangle == Some(dnd_id) => { let state = state(); // ASHLEY TODO we should be able to reset but for now we don't if we are handling a // drop @@ -1954,13 +1905,9 @@ where return event::Status::Captured; } #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::DndData { mime_type, data }, - ))) => { - let Some(on_dnd_command_produced) = on_dnd_command_produced else { - return event::Status::Ignored; - }; - + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) + if rectangle == Some(dnd_id) => + { let state = state(); if let DndOfferState::Dropped = state.dnd_offer.clone() { state.dnd_offer = DndOfferState::None; @@ -1982,9 +1929,6 @@ where shell.publish(message); } - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::DndFinished - }))); let value = if is_secure { unsecured_value.secure() } else { @@ -1995,26 +1939,6 @@ where } return event::Status::Ignored; } - #[cfg(feature = "wayland")] - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::DndOffer( - wayland::DndOfferEvent::SourceActions(actions), - ))) => { - let Some(on_dnd_command_produced) = on_dnd_command_produced else { - return event::Status::Ignored; - }; - - let state = state(); - if let DndOfferState::HandlingOffer(..) = state.dnd_offer.clone() { - shell.publish(on_dnd_command_produced(Box::new(move || { - platform_specific::wayland::data_device::ActionInner::SetActions { - preferred: actions.intersection(DndAction::Move), - accepted: actions, - } - }))); - return event::Status::Captured; - } - return event::Status::Ignored; - } _ => {} } @@ -2032,6 +1956,7 @@ pub fn draw<'a, Message>( renderer: &mut crate::Renderer, theme: &crate::Theme, layout: Layout<'_>, + text_layout: Layout<'_>, cursor_position: mouse::Cursor, tree: &Tree, value: &Value, @@ -2083,7 +2008,7 @@ pub fn draw<'a, Message>( let mut children_layout = layout.children(); let bounds = layout.bounds(); - let text_bounds = children_layout.next().unwrap().bounds(); + let text_bounds = text_layout.bounds(); let is_mouse_over = cursor_position.is_over(bounds); @@ -2172,7 +2097,7 @@ pub fn draw<'a, Message>( if let (Some(label_layout), Some(label)) = (label_layout, label) { renderer.fill_text( Text { - content: label, + content: label.to_string(), size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), font: font.unwrap_or_else(|| renderer.default_font()), bounds: label_layout.bounds().size(), @@ -2180,7 +2105,7 @@ pub fn draw<'a, Message>( vertical_alignment: alignment::Vertical::Top, line_height, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, label_layout.bounds().position(), appearance.label_color, @@ -2214,14 +2139,24 @@ pub fn draw<'a, Message>( let size = size.unwrap_or_else(|| renderer.default_size().0); let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into(); - let (cursor, offset) = if let Some(focus) = &state.is_focused { + #[cfg(feature = "wayland")] + let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); + #[cfg(not(feature = "wayland"))] + let handling_dnd_offer = false; + let (cursor, offset) = if let Some(focus) = &state.is_focused.or_else(|| { + handling_dnd_offer.then(|| Focus { + updated_at: Instant::now(), + now: Instant::now(), + }) + }) { match state.cursor.state(value) { cursor::State::Index(position) => { let (text_value_width, offset) = - measure_cursor_and_scroll_offset(&state.value, text_bounds, position); + measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position); - let is_cursor_visible = - ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) % 2 + let is_cursor_visible = handling_dnd_offer + || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 == 0; if is_cursor_visible { @@ -2263,10 +2198,10 @@ pub fn draw<'a, Message>( let value_paragraph = &state.value; let (left_position, left_offset) = - measure_cursor_and_scroll_offset(value_paragraph, text_bounds, left); + measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left); let (right_position, right_offset) = - measure_cursor_and_scroll_offset(value_paragraph, text_bounds, right); + measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right); let width = right_position - left_position; @@ -2331,7 +2266,11 @@ pub fn draw<'a, Message>( renderer.fill_text( Text { - content: if text.is_empty() { placeholder } else { &text }, + content: if text.is_empty() { + placeholder.to_string() + } else { + text.clone() + }, font, bounds: bounds.size(), size: iced::Pixels(size), @@ -2339,7 +2278,7 @@ pub fn draw<'a, Message>( vertical_alignment: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, bounds.position(), color, @@ -2374,7 +2313,7 @@ pub fn draw<'a, Message>( if let (Some(helper_text_layout), Some(helper_text)) = (helper_text_layout, helper_text) { renderer.fill_text( Text { - content: helper_text, + content: helper_text.to_string(), // TODO remove to_string? size: iced::Pixels(helper_text_size), font, bounds: helper_text_layout.bounds().size(), @@ -2382,7 +2321,7 @@ pub fn draw<'a, Message>( vertical_alignment: alignment::Vertical::Top, line_height: helper_line_height, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }, helper_text_layout.bounds().position(), text_color, @@ -2414,11 +2353,23 @@ pub fn mouse_interaction( pub struct TextInputString(pub String); #[cfg(feature = "wayland")] -impl DataFromMimeType for TextInputString { - fn from_mime_type(&self, mime_type: &str) -> Option> { - SUPPORTED_TEXT_MIME_TYPES - .contains(&mime_type) - .then(|| self.0.as_bytes().to_vec()) +impl AsMimeTypes for TextInputString { + fn available(&self) -> Cow<'static, [String]> { + Cow::Owned( + SUPPORTED_TEXT_MIME_TYPES + .iter() + .cloned() + .map(String::from) + .collect::>(), + ) + } + + fn as_bytes(&self, mime_type: &str) -> Option> { + if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) { + Some(Cow::Owned(self.0.clone().as_bytes().to_vec())) + } else { + None + } } } @@ -2434,7 +2385,6 @@ pub(crate) enum DraggingState { pub(crate) enum DndOfferState { #[default] None, - OutsideWidget(Vec, DndAction), HandlingOffer(Vec, DndAction), Dropped, } @@ -2446,17 +2396,16 @@ pub(crate) struct DndOfferState; #[derive(Debug, Default, Clone)] #[must_use] pub struct State { - pub value: crate::Paragraph, - pub placeholder: crate::Paragraph, - pub label: crate::Paragraph, - pub helper_text: crate::Paragraph, + pub value: crate::Plain, + pub placeholder: crate::Plain, + pub label: crate::Plain, + pub helper_text: crate::Plain, pub dirty: bool, pub is_secure: bool, pub is_read_only: bool, select_on_focus: bool, is_focused: Option, dragging_state: Option, - #[cfg(feature = "wayland")] dnd_offer: DndOfferState, is_pasting: Option, last_click: Option, @@ -2522,15 +2471,14 @@ impl State { pub fn focused(is_secure: bool, is_read_only: bool) -> Self { Self { is_secure, - value: crate::Paragraph::new(), - placeholder: crate::Paragraph::new(), - label: crate::Paragraph::new(), - helper_text: crate::Paragraph::new(), + value: crate::Plain::default(), + placeholder: crate::Plain::default(), + label: crate::Plain::default(), + helper_text: crate::Plain::default(), is_read_only, is_focused: None, select_on_focus: false, dragging_state: None, - #[cfg(feature = "wayland")] dnd_offer: DndOfferState::default(), is_pasting: None, last_click: None, @@ -2654,6 +2602,7 @@ fn find_cursor_position( let char_offset = state .value + .raw() .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; @@ -2677,7 +2626,7 @@ fn replace_paragraph( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = crate::Paragraph::with_text(Text { + state.value = crate::Plain::new(Text { font, line_height, content: &value.to_string(), @@ -2686,7 +2635,7 @@ fn replace_paragraph( horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, - wrap: text::Wrap::default(), + wrapping: text::Wrapping::default(), }); } @@ -2714,7 +2663,7 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { }; let (_, offset) = - measure_cursor_and_scroll_offset(&state.value, text_bounds, focus_position); + measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position); offset } else { diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index 481d799..7f7634c 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use crate::widget::container; use crate::widget::Column; -use crate::Command; +use iced::{Padding, Task}; use iced_core::Element; use slotmap::new_key_type; use slotmap::SlotMap; @@ -47,15 +47,15 @@ pub fn toaster<'a, Message: Clone + 'static>( button::icon(icon::from_name("window-close-symbolic")) .on_press((toasts.on_close)(id)), ) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .spacing(space_xxs), ) - .align_items(iced::Alignment::Center) + .align_y(iced::Alignment::Center) .spacing(space_s); container(row) .padding([space_xxs, space_s, space_xxs, space_m]) - .style(crate::style::Container::Tooltip) + .class(crate::style::Container::Tooltip) }; let col = toasts @@ -175,7 +175,7 @@ impl Toasts { } /// Add a new [`Toast`] - pub fn push(&mut self, toast: Toast) -> Command { + pub fn push(&mut self, toast: Toast) -> Task { while self.toasts.len() >= self.limit { self.toasts.remove( self.queue @@ -200,7 +200,7 @@ impl Toasts { } #[cfg(not(feature = "tokio"))] { - Command::none() + Task::none() } } diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 7d9a58c..7a7f694 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -14,7 +14,6 @@ use iced_core::widget::Operation; use iced_core::Element; use iced_core::Overlay; use iced_core::{Clipboard, Layout, Length, Point, Rectangle, Shell, Vector, Widget}; -use iced_renderer::core::widget::OperationOutputWrapper; pub struct Toaster<'a, Message, Theme, Renderer> { toasts: Element<'a, Message, Theme, Renderer>, @@ -90,7 +89,7 @@ where state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation>, + operation: &mut dyn Operation<()>, ) { self.content .as_widget() @@ -142,22 +141,23 @@ where state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, + translation: Vector, ) -> Option> { //TODO: this hides the overlay of the content during the toast if self.is_empty { - self.content - .as_widget_mut() - .overlay(&mut state.children[0], layout, renderer) + self.content.as_widget_mut().overlay( + &mut state.children[0], + layout, + renderer, + translation, + ) } else { let bounds = layout.bounds(); - Some(overlay::Element::new( - bounds.position(), - Box::new(ToasterOverlay::new( - &mut state.children[1], - &mut self.toasts, - )), - )) + Some(overlay::Element::new(Box::new(ToasterOverlay::new( + &mut state.children[1], + &mut self.toasts, + )))) } } @@ -166,7 +166,7 @@ where state: &Tree, layout: Layout<'_>, renderer: &Renderer, - dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.content.as_widget().drag_destinations( &state.children[0], @@ -196,13 +196,7 @@ impl<'a, 'b, Message, Theme, Renderer> Overlay where Renderer: renderer::Renderer, { - fn layout( - &mut self, - renderer: &Renderer, - bounds: Size, - position: Point, - _translation: Vector, - ) -> Node { + fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { let limits = Limits::new(Size::ZERO, bounds); let mut node = self @@ -276,7 +270,7 @@ where ) -> Option> { self.element .as_widget_mut() - .overlay(self.state, layout, renderer) + .overlay(self.state, layout, renderer, Default::default()) } } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 4e92ffd..972712a 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -4,15 +4,15 @@ use iced::{widget, Length}; use iced_core::text; -pub fn toggler<'a, Message, Theme: iced_widget::toggler::StyleSheet, Renderer>( - label: impl Into>, +pub fn toggler<'a, Message, Theme: iced_widget::toggler::Catalog, Renderer>( is_checked: bool, f: impl Fn(bool) -> Message + 'a, ) -> widget::Toggler<'a, Message, Theme, Renderer> where Renderer: iced_core::Renderer + text::Renderer, { - widget::Toggler::new(label, is_checked, f) + widget::Toggler::new(is_checked) + .on_toggle(f) .size(24) .width(Length::Shrink) } diff --git a/src/widget/warning.rs b/src/widget/warning.rs index a6450d8..77d737a 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -41,9 +41,9 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> { widget::row::with_capacity(2) .push(label) .push(close_button) - .align_items(Alignment::Center) + .align_y(Alignment::Center) .apply(widget::container) - .style(theme::Container::custom(warning_container)) + .class(theme::Container::custom(warning_container)) .padding(10) .align_y(alignment::Vertical::Center) .width(Length::Fill) @@ -57,9 +57,9 @@ impl<'a, Message: 'static + Clone> From> for Element<'a, Me } #[must_use] -pub fn warning_container(theme: &Theme) -> widget::container::Appearance { +pub fn warning_container(theme: &Theme) -> widget::container::Style { let cosmic = theme.cosmic(); - widget::container::Appearance { + widget::container::Style { 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())),