From 337b80d4ca02a63631668212bccbace22b8bb49f Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:56:21 -0400 Subject: [PATCH] feat: Tooltips and Better Surface Management --- Cargo.toml | 15 +- examples/applet/src/window.rs | 142 ++-- examples/application/Cargo.toml | 5 +- examples/application/src/main.rs | 160 ++++- examples/multi-window/src/window.rs | 7 +- examples/nav-context/src/main.rs | 6 +- examples/open-dialog/src/main.rs | 11 +- iced | 2 +- src/action.rs | 40 ++ src/app/action.rs | 75 +++ src/app/command.rs | 94 --- src/app/context_drawer.rs | 9 +- src/app/cosmic.rs | 398 ++++++++---- src/app/mod.rs | 338 ++-------- src/app/multi_window.rs | 3 + src/applet/mod.rs | 105 ++- src/command.rs | 45 ++ src/config/mod.rs | 4 +- src/{app => }/core.rs | 77 ++- src/dbus_activation.rs | 214 ++++++ src/desktop.rs | 5 +- src/ext.rs | 2 +- src/keyboard_nav.rs | 14 +- src/lib.rs | 25 +- src/malloc.rs | 3 + src/process.rs | 4 +- src/surface/action.rs | 152 +++++ src/surface/mod.rs | 85 +++ src/task.rs | 25 + src/task/mod.rs | 58 -- src/theme/portal.rs | 4 +- src/theme/style/iced.rs | 20 +- src/theme/style/menu_bar.rs | 8 +- src/theme/style/mod.rs | 5 + src/theme/style/tooltip.rs | 31 + src/widget/about.rs | 2 +- src/widget/aspect_ratio.rs | 7 +- src/widget/autosize.rs | 4 +- src/widget/button/icon.rs | 2 +- src/widget/button/image.rs | 2 +- src/widget/button/mod.rs | 2 +- src/widget/button/text.rs | 10 +- src/widget/button/widget.rs | 19 +- src/widget/calendar.rs | 14 +- src/widget/color_picker/mod.rs | 6 +- src/widget/context_drawer/overlay.rs | 3 +- src/widget/context_drawer/widget.rs | 2 +- src/widget/context_menu.rs | 4 +- src/widget/dialog.rs | 6 + src/widget/dnd_destination.rs | 4 +- src/widget/dnd_source.rs | 5 +- src/widget/dropdown/menu/mod.rs | 261 ++++++-- src/widget/dropdown/mod.rs | 38 +- src/widget/dropdown/multi/menu.rs | 8 +- src/widget/dropdown/multi/widget.rs | 6 +- src/widget/dropdown/widget.rs | 365 +++++++++-- src/widget/flex_row/widget.rs | 4 +- src/widget/grid/widget.rs | 14 +- src/widget/header_bar.rs | 17 +- src/widget/icon/mod.rs | 4 +- src/widget/icon/named.rs | 2 +- src/widget/id_container.rs | 4 +- src/widget/layer_container.rs | 3 +- src/widget/list/column.rs | 2 +- src/widget/menu.rs | 1 + src/widget/menu/key_bind.rs | 2 +- src/widget/menu/menu_bar.rs | 16 +- src/widget/menu/menu_inner.rs | 6 +- src/widget/menu/menu_tree.rs | 5 +- src/widget/mod.rs | 17 +- src/widget/nav_bar_toggle.rs | 2 +- src/widget/popover.rs | 10 +- src/widget/radio.rs | 2 +- src/widget/rectangle_tracker/mod.rs | 6 +- src/widget/responsive_container.rs | 299 +++++++++ src/widget/responsive_menu_bar.rs | 78 +++ src/widget/segmented_button/horizontal.rs | 4 +- src/widget/segmented_button/model/entity.rs | 2 +- src/widget/segmented_button/vertical.rs | 4 +- src/widget/segmented_button/widget.rs | 8 +- src/widget/settings/section.rs | 4 +- src/widget/spin_button.rs | 12 +- src/widget/text_input/input.rs | 146 +++-- src/widget/text_input/value.rs | 2 +- src/widget/toaster/mod.rs | 2 +- src/widget/toaster/widget.rs | 8 +- src/widget/wayland/mod.rs | 1 + src/widget/wayland/tooltip/mod.rs | 76 +++ src/widget/wayland/tooltip/widget.rs | 684 ++++++++++++++++++++ src/widget/wrapper.rs | 220 +++++++ 90 files changed, 3651 insertions(+), 977 deletions(-) create mode 100644 src/action.rs create mode 100644 src/app/action.rs delete mode 100644 src/app/command.rs create mode 100644 src/command.rs rename src/{app => }/core.rs (84%) create mode 100644 src/dbus_activation.rs create mode 100644 src/surface/action.rs create mode 100644 src/surface/mod.rs create mode 100644 src/task.rs delete mode 100644 src/task/mod.rs create mode 100644 src/theme/style/tooltip.rs create mode 100644 src/widget/responsive_container.rs create mode 100644 src/widget/responsive_menu_bar.rs create mode 100644 src/widget/wayland/mod.rs create mode 100644 src/widget/wayland/tooltip/mod.rs create mode 100644 src/widget/wayland/tooltip/widget.rs create mode 100644 src/widget/wrapper.rs diff --git a/Cargo.toml b/Cargo.toml index c70b75bc..7649e5d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,11 +66,14 @@ tokio = [ # Wayland window support wayland = [ "ashpd?/wayland", + "autosize", "iced_runtime/wayland", "iced/wayland", "iced_winit/wayland", "cctk", + "surface-message", ] +surface-message = [] # multi-window support multi-window = ["iced/multi-window"] # Render with wgpu @@ -84,11 +87,19 @@ winit_wgpu = ["winit", "wgpu"] xdg-portal = ["ashpd"] qr_code = ["iced/qr_code"] markdown = ["iced/markdown"] +async-std = [ + "dep:async-std", + "ashpd/async-std", + "rfd?/async-std", + "zbus?/async-io", + "iced/async-std", +] [dependencies] apply = "0.3.0" ashpd = { version = "0.9.1", default-features = false, optional = true } async-fs = { version = "2.1", optional = true } +async-std = { version = "1.10", optional = true } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "178eb0b", optional = true } chrono = "0.4.35" cosmic-config = { path = "cosmic-config" } @@ -104,7 +115,9 @@ libc = { version = "0.2.155", optional = true } license = { version = "3.5.1", optional = true } mime = { version = "0.3.17", optional = true } palette = "0.7.3" -rfd = { version = "0.14.0", default-features = false, features = ["xdg-portal"], optional = true } +rfd = { version = "0.14.0", default-features = false, features = [ + "xdg-portal", +], optional = true } rustix = { version = "0.38.34", features = [ "pipe", "process", diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 4f34ddfb..15085d85 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,27 +1,38 @@ -use cosmic::app::Core; -use cosmic::iced::application; -use cosmic::iced::platform_specific::shell::commands::popup::{destroy_popup, get_popup}; +use cosmic::app::{Core, Task}; + use cosmic::iced::window::Id; -use cosmic::iced::{Length, Limits, Task}; +use cosmic::iced::Length; use cosmic::iced_runtime::core::window; -use cosmic::theme::iced; -use cosmic::widget::{list_column, settings, toggler}; -use cosmic::{Element, Theme}; +use cosmic::surface::action::{app_popup, destroy_popup}; +use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; +use cosmic::Element; const ID: &str = "com.system76.CosmicAppletExample"; -#[derive(Default)] pub struct Window { core: Core, popup: Option, example_row: bool, + selected: Option, +} + +impl Default for Window { + fn default() -> Self { + Self { + core: Core::default(), + popup: None, + example_row: false, + selected: None, + } + } } #[derive(Clone, Debug)] pub enum Message { - TogglePopup, PopupClosed(Id), ToggleExampleRow(bool), + Selected(usize), + Surface(cosmic::surface::Action), } impl cosmic::Application for Window { @@ -38,7 +49,7 @@ impl cosmic::Application for Window { &mut self.core } - fn init(core: Core, _flags: Self::Flags) -> (Self, Task>) { + fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { let window = Window { core, ..Default::default() @@ -50,60 +61,85 @@ impl cosmic::Application for Window { Some(Message::PopupClosed(id)) } - fn update(&mut self, message: Self::Message) -> Task> { + fn update(&mut self, message: Message) -> Task { match message { - Message::TogglePopup => { - return if let Some(p) = self.popup.take() { - destroy_popup(p) - } else { - let new_id = Id::unique(); - self.popup.replace(new_id); - 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) - .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) { self.popup = None; } } - Message::ToggleExampleRow(toggled) => self.example_row = toggled, - } + Message::ToggleExampleRow(toggled) => { + self.example_row = toggled; + } + + Message::Surface(a) => { + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(a), + )); + } + Message::Selected(i) => { + self.selected = Some(i); + } + }; Task::none() } - fn view(&self) -> Element { - self.core - .applet - .icon_button("display-symbolic") - .on_press(Message::TogglePopup) - .into() + fn view(&self) -> Element { + let btn = self.core.applet.icon_button("display-symbolic").on_press( + if let Some(id) = self.popup { + Message::Surface(destroy_popup(id)) + } else { + Message::Surface(app_popup::( + |state: &mut Window| { + let new_id = Id::unique(); + state.popup = Some(new_id); + let popup_settings = state.core.applet.get_popup_settings( + state.core.main_window_id().unwrap(), + new_id, + None, + None, + None, + ); + + popup_settings + }, + Some(Box::new(move |state: &Window| { + let content_list = list_column() + .padding(5) + .spacing(0) + .add(settings::item( + "Example row", + cosmic::widget::container( + toggler(state.example_row) + .on_toggle(|value| Message::ToggleExampleRow(value)), + ) + .height(Length::Fixed(50.)), + )) + .add(popup_dropdown( + &["1", "asdf", "hello", "test"], + state.selected, + Message::Selected, + state.popup.unwrap_or(Id::NONE), + Message::Surface, + |m| m, + )); + Element::from(state.core.applet.popup_container(content_list)) + .map(cosmic::Action::App) + })), + )) + }, + ); + + Element::from(self.core.applet.applet_tooltip::( + btn, + "test", + self.popup.is_some(), + |a| Message::Surface(a), + )) } - fn view_window(&self, _id: Id) -> Element { - let content_list = list_column().padding(5).spacing(0).add(settings::item( - "Example row", - cosmic::widget::container( - toggler(self.example_row).on_toggle(|value| Message::ToggleExampleRow(value)), - ) - .height(Length::Fixed(50.)), - )); - - self.core.applet.popup_container(content_list).into() + fn view_window(&self, _id: Id) -> Element { + "oops".into() } fn style(&self) -> Option { diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 695f9897..23413086 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -3,6 +3,10 @@ name = "application" version = "0.1.0" edition = "2021" +[features] +default = ["wayland"] +wayland = ["libcosmic/wayland"] + [dependencies] tracing = "0.1.37" tracing-subscriber = "0.3.17" @@ -18,7 +22,6 @@ features = [ "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 a77b9d74..bcffc316 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -3,12 +3,27 @@ //! Application API example +use std::collections::HashMap; +use std::sync::LazyLock; + use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::widget::column; +use cosmic::iced::Length; use cosmic::iced_core::Size; -use cosmic::widget::nav_bar; +use cosmic::widget::icon::{from_name, Handle}; +use cosmic::widget::menu::KeyBind; +use cosmic::widget::{button, text}; +use cosmic::widget::{ + container, + menu::menu_button, + menu::{self, action::MenuAction}, + nav_bar, responsive, +}; use cosmic::{executor, iced, ApplicationExt, Element}; +static MENU_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("menu_id")); + #[derive(Clone, Copy)] pub enum Page { Page1, @@ -28,11 +43,24 @@ impl Page { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Action { + Hi, +} + +impl MenuAction for Action { + type Message = Message; + + fn message(&self) -> Message { + Message::Hi + } +} + /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); + // tracing_subscriber::fmt::init(); + // let _ = tracing_log::LogTracer::init(); let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), @@ -56,6 +84,8 @@ pub enum Message { Input2(String), Ignore, ToggleHide, + Surface(cosmic::surface::Action), + Hi, } /// The [`App`] stores application-specific state. @@ -65,6 +95,7 @@ pub struct App { input_1: String, input_2: String, hidden: bool, + keybinds: HashMap, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -105,6 +136,7 @@ impl cosmic::Application for App { input_1: String::new(), input_2: String::new(), hidden: true, + keybinds: HashMap::new(), }; let command = app.update_title(); @@ -136,6 +168,12 @@ impl cosmic::Application for App { Message::ToggleHide => { self.hidden = !self.hidden; } + Message::Surface(_) => { + // unimplemented!() + } + Message::Hi => { + dbg!("hi"); + } } Task::none() } @@ -178,6 +216,122 @@ impl cosmic::Application for App { Element::from(centered) } + + fn header_start(&self) -> Vec> { + use cosmic::widget::menu::Tree; + #[cfg(not(feature = "wayland"))] + { + vec![cosmic::widget::menu::bar(vec![ + Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiiii 1"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi", None, Action::Hi)], + ), + ), + Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiii 2"), + menu::items( + &self.keybinds, + vec![menu::Item::Button("hi 2", None, Action::Hi)], + ), + ), + Tree::with_children( + menu::root("hiiiiiiiiiiiiiiiiiiiii 3"), + menu::items( + &self.keybinds, + vec![ + menu::Item::Button("hi 3", None, Action::Hi), + menu::Item::Button("hi 3 #2", None, Action::Hi), + ], + ), + ), + Tree::with_children( + menu::root("hi 3"), + menu::items( + &self.keybinds, + vec![ + menu::Item::Button("hi 3", None, Action::Hi), + menu::Item::Button("hi 3 #2", None, Action::Hi), + menu::Item::Button("hi 3 #3", None, Action::Hi), + ], + ), + ), + Tree::with_children( + menu::root("hi 4"), + menu::items( + &self.keybinds, + vec![ + menu::Item::Folder( + "hi 41 extra root", + vec![menu::Item::Button("hi 3", None, Action::Hi)], + ), + menu::Item::Button("hi 42", None, Action::Hi), + menu::Item::Button("hi 43", None, Action::Hi), + menu::Item::Button("hi 44", None, Action::Hi), + menu::Item::Button("hi 45", None, Action::Hi), + menu::Item::Button("hi 46", None, Action::Hi), + ], + ), + ), + ]) + .into()] + } + #[cfg(feature = "wayland")] + { + vec![cosmic::widget::responsive_menu_bar( + self.core(), + &self.keybinds, + MENU_ID.clone(), + Message::Surface, + vec![ + ( + "hiiiiiiiiiiiiiiiiiii 1".into(), + vec![menu::Item::Button("hi 1".into(), None, Action::Hi)], + ), + ( + "hiiiiiiiiiiiiiiiiiii 2".into(), + vec![ + menu::Item::Button("hi 2".into(), None, Action::Hi), + menu::Item::Button("hi 22".into(), None, Action::Hi), + ], + ), + ( + "hiiiiiiiiiiiiiiiiiii 3".into(), + vec![ + menu::Item::Button("hi 3".into(), None, Action::Hi), + menu::Item::Button("hi 33".into(), None, Action::Hi), + menu::Item::Button("hi 333".into(), None, Action::Hi), + ], + ), + ( + "hiiiiiiiiiiiiiiiiiii 4".into(), + vec![ + menu::Item::Button("hi 4".into(), None, Action::Hi), + menu::Item::Button("hi 44".into(), None, Action::Hi), + menu::Item::Button("hi 444".into(), None, Action::Hi), + menu::Item::Folder( + "nest 4".into(), + vec![ + menu::Item::Button("hi 4".into(), None, Action::Hi), + menu::Item::Button("hi 44".into(), None, Action::Hi), + menu::Item::Button("hi 444".into(), None, Action::Hi), + menu::Item::Folder( + "nest 2 4".into(), + vec![ + menu::Item::Button("hi 4".into(), None, Action::Hi), + menu::Item::Button("hi 44".into(), None, Action::Hi), + menu::Item::Button("hi 444".into(), None, Action::Hi), + ], + ), + ], + ), + ], + ), + ], + )] + } + } } impl App diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 15663ea3..96d166d4 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -74,10 +74,7 @@ impl cosmic::Application for MultiWindow { }) } - fn update( - &mut self, - message: Self::Message, - ) -> iced::Task> { + fn update(&mut self, message: Self::Message) -> iced::Task> { match message { Message::CloseWindow(id) => window::close(id), Message::WindowClosed(id) => { @@ -110,7 +107,7 @@ impl cosmic::Application for MultiWindow { ); _ = self.set_window_title(format!("window_{}", count), id); - spawn_window.map(|id| cosmic::app::Message::App(Message::WindowOpened(id, None))) + spawn_window.map(|id| cosmic::Action::App(Message::WindowOpened(id, None))) } Message::Input(id, value) => { if let Some((_, w)) = self.windows.iter_mut().find(|e| e.1.input_id == id) { diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 58e20849..be458171 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -70,10 +70,10 @@ pub enum NavMenuAction { } impl menu::Action for NavMenuAction { - type Message = cosmic::app::Message; + type Message = cosmic::Action; fn message(&self) -> Self::Message { - cosmic::app::Message::App(Message::NavMenuAction(*self)) + cosmic::Action::App(Message::NavMenuAction(*self)) } } @@ -131,7 +131,7 @@ impl cosmic::Application for App { fn nav_context_menu( &self, id: nav_bar::Id, - ) -> Option>>> { + ) -> Option>>> { Some(menu::items( &HashMap::new(), vec![ diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 5ae2df47..0edac466 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -34,6 +34,7 @@ pub enum Message { OpenError(Arc), OpenFile, Selected(Url), + Surface(cosmic::surface::Action), } /// The [`App`] stores application-specific state. @@ -91,13 +92,11 @@ impl cosmic::Application for App { Message::Cancelled => { eprintln!("open file dialog cancelled"); } - Message::FileRead(url, contents) => { eprintln!("read file"); self.selected_file = Some(url); self.file_contents = contents; } - Message::Selected(url) => { eprintln!("selected file"); @@ -142,8 +141,6 @@ impl cosmic::Application for App { Message::FileRead(url, contents) }); } - - // Creates a new open dialog. Message::OpenFile => { return cosmic::task::future(async move { eprintln!("opening new dialog"); @@ -169,13 +166,9 @@ impl cosmic::Application for App { } }); } - - // Displays an error in the application's warning bar. Message::Error(why) => { self.error_status = Some(why); } - - // Displays an error in the application's warning bar. Message::OpenError(why) => { if let Some(why) = Arc::into_inner(why) { let mut source: &dyn std::error::Error = &why; @@ -190,10 +183,10 @@ impl cosmic::Application for App { self.error_status = Some(string); } } - Message::CloseError => { self.error_status = None; } + Message::Surface(surface) => {} } Task::none() diff --git a/iced b/iced index a8c31a09..07880754 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a8c31a0982b134278e7ecb5a91e0d3ba0fa6d2dd +Subproject commit 0788075435aa7a6532327b9435fcfc7a12e1b6a6 diff --git a/src/action.rs b/src/action.rs new file mode 100644 index 00000000..b7162896 --- /dev/null +++ b/src/action.rs @@ -0,0 +1,40 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +#[cfg(feature = "winit")] +use crate::app; +#[cfg(feature = "single-instance")] +use crate::dbus_activation; + +pub const fn app(message: M) -> Action { + Action::App(message) +} +#[cfg(feature = "winit")] +pub const fn cosmic(message: app::Action) -> Action { + Action::Cosmic(message) +} + +pub const fn none() -> Action { + Action::None +} + +#[derive(Clone, Debug)] +#[must_use] +pub enum Action { + /// Messages from the application, for the application. + App(M), + #[cfg(feature = "winit")] + /// Internal messages to be handled by libcosmic. + Cosmic(app::Action), + #[cfg(feature = "single-instance")] + /// Dbus activation messages + DbusActivation(dbus_activation::Message), + /// Do nothing + None, +} + +impl From for Action { + fn from(value: M) -> Self { + Self::App(value) + } +} diff --git a/src/app/action.rs b/src/app/action.rs new file mode 100644 index 00000000..44655ffa --- /dev/null +++ b/src/app/action.rs @@ -0,0 +1,75 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::surface; +use crate::theme::Theme; +use crate::widget::nav_bar; +use crate::{config::CosmicTk, keyboard_nav}; +#[cfg(feature = "wayland")] +use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; +use cosmic_theme::ThemeMode; +#[cfg(not(any(feature = "multi-window", feature = "wayland")))] +use iced::Application as IcedApplication; + +/// A message managed internally by COSMIC. +#[derive(Clone, Debug)] +pub enum Action { + /// Activate the application + Activate(String), + /// Application requests theme change. + AppThemeChange(Theme), + /// Requests to close the window. + Close, + /// Closes or shows the context drawer. + ContextDrawer(bool), + /// Requests to drag the window. + Drag, + /// Window focus changed + Focus(iced::window::Id), + /// Keyboard shortcuts managed by libcosmic. + KeyboardNav(keyboard_nav::Action), + /// Requests to maximize the window. + Maximize, + /// Requests to minimize the window. + Minimize, + /// Activates a navigation element from the nav bar. + NavBar(nav_bar::Id), + /// Activates a context menu for an item from the nav bar. + NavBarContext(nav_bar::Id), + /// Set scaling factor + ScaleFactor(f32), + /// Show the window menu + ShowWindowMenu, + /// Tracks updates to window suggested size. + #[cfg(feature = "applet")] + SuggestedBounds(Option), + /// Internal surface message + Surface(surface::Action), + /// Notifies that a surface was closed. + /// Any data relating to the surface should be cleaned up. + SurfaceClosed(iced::window::Id), + /// Notification of system theme changes. + SystemThemeChange(Vec<&'static str>, Theme), + /// Notification of system theme mode changes. + SystemThemeModeChange(Vec<&'static str>, ThemeMode), + /// Toggles visibility of the nav bar. + ToggleNavBar, + /// Toggles the condensed status of the nav bar. + ToggleNavBarCondensed, + /// Toolkit configuration update + ToolkitConfig(CosmicTk), + /// Window focus lost + Unfocus(iced::window::Id), + /// Updates the window maximized state + WindowMaximized(iced::window::Id, bool), + /// Updates the tracked window geometry. + WindowResize(iced::window::Id, f32, f32), + /// Tracks updates to window state. + #[cfg(feature = "wayland")] + WindowState(iced::window::Id, WindowState), + /// Capabilities the window manager supports + #[cfg(feature = "wayland")] + WmCapabilities(iced::window::Id, WindowManagerCapabilities), + #[cfg(feature = "xdg-portal")] + DesktopSettings(crate::theme::portal::Desktop), +} diff --git a/src/app/command.rs b/src/app/command.rs deleted file mode 100644 index b39118f5..00000000 --- a/src/app/command.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced::window; - -/// Asynchronous actions for COSMIC applications. -use super::Message; - -/// Commands for COSMIC applications. -pub type Task = iced::Task>; - -/// Creates a task which yields a [`crate::app::Message`]. -pub fn message(message: Message) -> Task { - crate::task::message(message) -} - -/// Convenience methods for building message-based commands. -pub mod message { - /// Creates a task which yields an application message. - pub fn app(message: M) -> crate::app::Task { - super::message(super::Message::App(message)) - } - - /// Creates a task which yields a cosmic message. - pub fn cosmic(message: crate::app::cosmic::Message) -> crate::app::Task { - super::message(super::Message::Cosmic(message)) - } -} - -impl crate::app::Core { - pub fn drag(&self, id: Option) -> iced::Task> { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - crate::task::drag(id).map(Message::Cosmic) - } - - pub fn maximize( - &self, - id: Option, - maximized: bool, - ) -> iced::Task> { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - crate::task::maximize(id, maximized).map(Message::Cosmic) - } - - pub fn minimize(&self, id: Option) -> iced::Task> { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - crate::task::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) else { - return iced::Task::none(); - }; - crate::task::set_title(id, title).map(Message::Cosmic) - } - - pub fn set_windowed( - &self, - id: Option, - ) -> iced::Task> { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - crate::task::set_windowed(id).map(Message::Cosmic) - } - - pub fn toggle_maximize( - &self, - id: Option, - ) -> iced::Task> { - let Some(id) = id.or(self.main_window) else { - return iced::Task::none(); - }; - crate::task::toggle_maximize(id).map(Message::Cosmic) - } -} - -pub fn set_theme(theme: crate::Theme) -> iced::Task> { - message::cosmic(super::cosmic::Message::AppThemeChange(theme)) -} diff --git a/src/app/context_drawer.rs b/src/app/context_drawer.rs index af937168..bb681242 100644 --- a/src/app/context_drawer.rs +++ b/src/app/context_drawer.rs @@ -1,3 +1,6 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 +// use std::borrow::Cow; use crate::Element; @@ -12,11 +15,11 @@ pub struct ContextDrawer<'a, Message: Clone + 'static> { } #[cfg(feature = "about")] -pub fn about<'a, Message: Clone + 'static>( - about: &'a crate::widget::about::About, +pub fn about( + about: &crate::widget::about::About, on_url_press: impl Fn(String) -> Message, on_close: Message, -) -> ContextDrawer<'a, Message> { +) -> ContextDrawer<'_, Message> { context_drawer(crate::widget::about(about, on_url_press), on_close) } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 42a16445..919e6042 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; +use std::collections::HashMap; use std::sync::Arc; -use super::{Application, ApplicationExt, Core, Subscription}; -use crate::config::CosmicTk; +use super::{Action, Application, ApplicationExt, Subscription}; use crate::theme::{Theme, ThemeType, THEME}; -use crate::widget::nav_bar; -use crate::{keyboard_nav, Element}; +use crate::{keyboard_nav, Core, Element}; #[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; @@ -20,69 +19,14 @@ use iced::{window, Task}; use iced_futures::event::listen_with; use palette::color_difference::EuclideanDistance; -/// A message managed internally by COSMIC. -#[derive(Clone, Debug)] -pub enum Message { - /// Application requests theme change. - AppThemeChange(Theme), - /// Requests to close the window. - Close, - /// Closes or shows the context drawer. - ContextDrawer(bool), - /// Requests to drag the window. - Drag, - /// Keyboard shortcuts managed by libcosmic. - KeyboardNav(keyboard_nav::Message), - /// Requests to maximize the window. - Maximize, - /// Requests to minimize the window. - Minimize, - /// Activates a navigation element from the nav bar. - NavBar(nav_bar::Id), - /// Activates a context menu for an item from the nav bar. - NavBarContext(nav_bar::Id), - /// Set scaling factor - ScaleFactor(f32), - /// Notification of system theme changes. - SystemThemeChange(Vec<&'static str>, Theme), - /// Notification of system theme mode changes. - SystemThemeModeChange(Vec<&'static str>, ThemeMode), - /// Toggles visibility of the nav bar. - ToggleNavBar, - /// Toggles the condensed status of the nav bar. - ToggleNavBarCondensed, - /// Toolkit configuration update - ToolkitConfig(CosmicTk), - /// Updates the window maximized state - WindowMaximized(window::Id, bool), - /// Updates the tracked window geometry. - WindowResize(window::Id, f32, f32), - /// Tracks updates to window state. - #[cfg(feature = "wayland")] - WindowState(window::Id, WindowState), - /// Capabilities the window manager supports - #[cfg(feature = "wayland")] - WmCapabilities(window::Id, WindowManagerCapabilities), - /// Notifies that a surface was closed. - /// Any data relating to the surface should be cleaned up. - SurfaceClosed(window::Id), - /// Activate the application - Activate(String), - ShowWindowMenu, - #[cfg(feature = "xdg-portal")] - DesktopSettings(crate::theme::portal::Desktop), - /// Window focus changed - Focus(window::Id), - /// Window focus lost - Unfocus(window::Id), - /// Tracks updates to window suggested size. - #[cfg(feature = "applet")] - SuggestedBounds(Option), -} - #[derive(Default)] -pub struct Cosmic { +pub struct Cosmic { pub app: App, + #[cfg(feature = "wayland")] + pub surface_views: HashMap< + window::Id, + Box Fn(&'a App) -> Element<'a, crate::Action>>, + >, } impl Cosmic @@ -91,7 +35,7 @@ where { pub fn init( (mut core, flags): (Core, T::Flags), - ) -> (Self, iced::Task>) { + ) -> (Self, iced::Task>) { #[cfg(feature = "dbus-config")] { use iced_futures::futures::executor::block_on; @@ -113,16 +57,157 @@ where self.app.title(id).to_string() } + pub fn surface_update( + &mut self, + _surface_message: crate::surface::Action, + ) -> iced::Task> { + #[cfg(feature = "wayland")] + match _surface_message { + crate::surface::Action::AppSubsurface(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for subsurface"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + + Send + + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for subsurface view: {err:?}"); + + None + } + } + }) { + let settings = settings(&mut self.app); + self.get_subsurface(settings, *view) + } else { + iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) + } + } + crate::surface::Action::Subsurface(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for subsurface"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for subsurface view: {err:?}"); + + None + } + } + }) { + let settings = settings(); + self.get_subsurface(settings, Box::new(move |_| view())) + } else { + iced_winit::commands::subsurface::get_subsurface(settings()) + } + } + crate::surface::Action::AppPopup(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for popup"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + + Send + + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for subsurface view: {err:?}"); + None + } + } + }) { + let settings = settings(&mut self.app); + + self.get_popup(settings, *view) + } else { + iced_winit::commands::popup::get_popup(settings(&mut self.app)) + } + } + #[cfg(feature = "wayland")] + crate::surface::Action::DestroyPopup(id) => { + iced_winit::commands::popup::destroy_popup(id) + } + #[cfg(feature = "wayland")] + crate::surface::Action::DestroySubsurface(id) => { + iced_winit::commands::subsurface::destroy_subsurface(id) + } + crate::surface::Action::ResponsiveMenuBar { + menu_bar, + limits, + size, + } => { + let core = self.app.core_mut(); + core.menu_bars.insert(menu_bar, (limits, size)); + iced::Task::none() + } + crate::surface::Action::Popup(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for popup"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for subsurface view: {err:?}"); + None + } + } + }) { + let settings = settings(); + + self.get_popup(settings, Box::new(move |_| view())) + } else { + iced_winit::commands::popup::get_popup(settings()) + } + } + crate::surface::Action::Ignore => iced::Task::none(), + crate::surface::Action::Task(f) => { + f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) + } + } + + #[cfg(not(feature = "wayland"))] + iced::Task::none() + } + pub fn update( &mut self, - message: super::Message, - ) -> iced::Task> { + message: crate::Action, + ) -> iced::Task> { let message = match message { - super::Message::App(message) => self.app.update(message), - super::Message::Cosmic(message) => self.cosmic_update(message), - super::Message::None => iced::Task::none(), + crate::Action::App(message) => self.app.update(message), + crate::Action::Cosmic(message) => self.cosmic_update(message), + crate::Action::None => iced::Task::none(), #[cfg(feature = "single-instance")] - super::Message::DbusActivation(message) => self.app.dbus_activation(message), + crate::Action::DbusActivation(message) => self.app.dbus_activation(message), }; #[cfg(target_env = "gnu")] @@ -158,29 +243,29 @@ where } #[allow(clippy::too_many_lines)] - pub fn subscription(&self) -> Subscription> { + pub fn subscription(&self) -> Subscription> { let window_events = listen_with(|event, _, id| { match event { iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => { - return Some(Message::WindowResize(id, width, height)); + return Some(Action::WindowResize(id, width, height)); } iced::Event::Window(window::Event::Closed) => { - return Some(Message::SurfaceClosed(id)); + return Some(Action::SurfaceClosed(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::Focused) => return Some(Action::Focus(id)), + iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), #[cfg(feature = "wayland")] 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)); + return Some(Action::SurfaceClosed(id)); } #[cfg(feature = "applet")] wayland::Event::Window( iced::event::wayland::WindowEvent::SuggestedBounds(b), ) => { - return Some(Message::SuggestedBounds(b)); + return Some(Action::SuggestedBounds(b)); } _ => (), } @@ -192,7 +277,7 @@ where }); let mut subscriptions = vec![ - self.app.subscription().map(super::Message::App), + self.app.subscription().map(crate::Action::App), self.app .core() .watch_config::(crate::config::ID) @@ -205,7 +290,7 @@ where tracing::error!(?why, "cosmic toolkit config update error"); } - super::Message::Cosmic(Message::ToolkitConfig(update.config)) + crate::Action::Cosmic(Action::ToolkitConfig(update.config)) }), self.app .core() @@ -232,12 +317,12 @@ where { tracing::error!(?why, "cosmic theme config update error"); } - Message::SystemThemeChange( + Action::SystemThemeChange( update.keys, crate::theme::Theme::system(Arc::new(update.config)), ) }) - .map(super::Message::Cosmic), + .map(crate::Action::Cosmic), self.app .core() .watch_config::(cosmic_theme::THEME_MODE_ID) @@ -249,27 +334,27 @@ where { tracing::error!(?error, "error reading system theme mode update"); } - Message::SystemThemeModeChange(update.keys, update.config) + Action::SystemThemeModeChange(update.keys, update.config) }) - .map(super::Message::Cosmic), - window_events.map(super::Message::Cosmic), + .map(crate::Action::Cosmic), + window_events.map(crate::Action::Cosmic), #[cfg(feature = "xdg-portal")] crate::theme::portal::desktop_settings() - .map(Message::DesktopSettings) - .map(super::Message::Cosmic), + .map(Action::DesktopSettings) + .map(crate::Action::Cosmic), ]; if self.app.core().keyboard_nav { subscriptions.push( keyboard_nav::subscription() - .map(Message::KeyboardNav) - .map(super::Message::Cosmic), + .map(Action::KeyboardNav) + .map(crate::Action::Cosmic), ); } #[cfg(feature = "single-instance")] if self.app.core().single_instance { - subscriptions.push(super::single_instance_subscription::()); + subscriptions.push(crate::dbus_activation::subscription::()); } Subscription::batch(subscriptions) @@ -286,20 +371,24 @@ where } #[cfg(feature = "multi-window")] - pub fn view(&self, id: window::Id) -> Element> { + pub fn view(&self, id: window::Id) -> Element> { + #[cfg(feature = "wayland")] + if let Some(v) = self.surface_views.get(&id) { + return v(&self.app); + } 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); + return self.app.view_window(id).map(crate::Action::App); } let view = if self.app.core().window.use_template { self.app.view_main() } else { - self.app.view().map(super::Message::App) + self.app.view().map(crate::Action::App) }; #[cfg(target_env = "gnu")] @@ -309,7 +398,7 @@ where } #[cfg(not(feature = "multi-window"))] - pub fn view(&self) -> Element> { + pub fn view(&self) -> Element> { let view = self.app.view_main(); #[cfg(target_env = "gnu")] @@ -321,7 +410,7 @@ where impl Cosmic { #[allow(clippy::unused_self)] - pub fn close(&mut self) -> iced::Task> { + pub fn close(&mut self) -> iced::Task> { if let Some(id) = self.app.core().main_window_id() { iced::window::close(id) } else { @@ -330,9 +419,9 @@ impl Cosmic { } #[allow(clippy::too_many_lines)] - fn cosmic_update(&mut self, message: Message) -> iced::Task> { + fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { - Message::WindowMaximized(id, maximized) => { + Action::WindowMaximized(id, maximized) => { if self .app .core() @@ -343,7 +432,7 @@ impl Cosmic { } } - Message::WindowResize(id, width, height) => { + Action::WindowResize(id, width, height) => { if self .app .core() @@ -358,12 +447,12 @@ impl Cosmic { //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) return iced::window::get_maximized(id).map(move |maximized| { - super::Message::Cosmic(Message::WindowMaximized(id, maximized)) + crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) }); } #[cfg(feature = "wayland")] - Message::WindowState(id, state) => { + Action::WindowState(id, state) => { if self .app .core() @@ -383,7 +472,7 @@ impl Cosmic { } #[cfg(feature = "wayland")] - Message::WmCapabilities(id, capabilities) => { + Action::WmCapabilities(id, capabilities) => { if self .app .core() @@ -399,49 +488,49 @@ impl Cosmic { } } - Message::KeyboardNav(message) => match message { - keyboard_nav::Message::FocusNext => { - return iced::widget::focus_next().map(super::Message::Cosmic) + Action::KeyboardNav(message) => match message { + keyboard_nav::Action::FocusNext => { + return iced::widget::focus_next().map(crate::Action::Cosmic) } - keyboard_nav::Message::FocusPrevious => { - return iced::widget::focus_previous().map(super::Message::Cosmic) + keyboard_nav::Action::FocusPrevious => { + return iced::widget::focus_previous().map(crate::Action::Cosmic) } - keyboard_nav::Message::Escape => return self.app.on_escape(), - keyboard_nav::Message::Search => return self.app.on_search(), + keyboard_nav::Action::Escape => return self.app.on_escape(), + keyboard_nav::Action::Search => return self.app.on_search(), - keyboard_nav::Message::Fullscreen => return self.app.core().toggle_maximize(None), + keyboard_nav::Action::Fullscreen => return self.app.core().toggle_maximize(None), }, - Message::ContextDrawer(show) => { + Action::ContextDrawer(show) => { self.app.core_mut().set_show_context(show); return self.app.on_context_drawer(); } - Message::Drag => return self.app.core().drag(None), + Action::Drag => return self.app.core().drag(None), - Message::Minimize => return self.app.core().minimize(None), + Action::Minimize => return self.app.core().minimize(None), - Message::Maximize => return self.app.core().toggle_maximize(None), + Action::Maximize => return self.app.core().toggle_maximize(None), - Message::NavBar(key) => { + Action::NavBar(key) => { self.app.core_mut().nav_bar_set_toggled_condensed(false); return self.app.on_nav_select(key); } - Message::NavBarContext(key) => { + Action::NavBarContext(key) => { self.app.core_mut().nav_bar_set_context(key); return self.app.on_nav_context(key); } - Message::ToggleNavBar => { + Action::ToggleNavBar => { self.app.core_mut().nav_bar_toggle(); } - Message::ToggleNavBarCondensed => { + Action::ToggleNavBarCondensed => { self.app.core_mut().nav_bar_toggle_condensed(); } - Message::AppThemeChange(mut theme) => { + Action::AppThemeChange(mut theme) => { if let ThemeType::System { theme: _, .. } = theme.theme_type { self.app.core_mut().theme_sub_counter += 1; @@ -457,7 +546,7 @@ impl Cosmic { THEME.lock().unwrap().set_theme(theme.theme_type); } - Message::SystemThemeChange(keys, theme) => { + Action::SystemThemeChange(keys, theme) => { 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 { @@ -495,17 +584,17 @@ impl Cosmic { return cmd; } - Message::ScaleFactor(factor) => { + Action::ScaleFactor(factor) => { self.app.core_mut().set_scale_factor(factor); } - Message::Close => { + Action::Close => { return match self.app.on_app_exit() { Some(message) => self.app.update(message), None => self.close(), }; } - Message::SystemThemeModeChange(keys, mode) => { + Action::SystemThemeModeChange(keys, mode) => { if !keys.contains(&"is_dark") { return iced::Task::none(); } @@ -557,7 +646,7 @@ impl Cosmic { } return Task::batch(cmds); } - Message::Activate(_token) => + Action::Activate(_token) => { #[cfg(feature = "wayland")] if let Some(id) = self.app.core().main_window_id() { @@ -568,7 +657,10 @@ impl Cosmic { ); } } - Message::SurfaceClosed(id) => { + + Action::Surface(action) => return self.surface_update(action), + + Action::SurfaceClosed(id) => { let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) } else { @@ -578,17 +670,19 @@ impl Cosmic { 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::>()]); + ret = Task::batch(vec![iced::exit::>()]); } return ret; } - Message::ShowWindowMenu => { + + Action::ShowWindowMenu => { 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)) => { + Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { use ashpd::desktop::settings::ColorScheme; if match THEME.lock().unwrap().theme_type { ThemeType::System { @@ -628,7 +722,7 @@ impl Cosmic { } } #[cfg(feature = "xdg-portal")] - Message::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => { + Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => { use palette::Srgba; let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0); let core = self.app.core_mut(); @@ -657,11 +751,11 @@ impl Cosmic { } } #[cfg(feature = "xdg-portal")] - Message::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { + Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { // TODO when high contrast is integrated in settings and all custom themes } - Message::ToolkitConfig(config) => { + Action::ToolkitConfig(config) => { // Change the icon theme if not defined by the application. if !self.app.core().icon_theme_override && crate::icon_theme::default() != config.icon_theme @@ -672,18 +766,18 @@ impl Cosmic { *crate::config::COSMIC_TK.write().unwrap() = config; } - Message::Focus(f) => { + Action::Focus(f) => { self.app.core_mut().focused_window = Some(f); } - Message::Unfocus(id) => { + Action::Unfocus(id) => { let core = self.app.core_mut(); if core.focused_window.as_ref().is_some_and(|cur| *cur == id) { core.focused_window = None; } } #[cfg(feature = "applet")] - Message::SuggestedBounds(b) => { + Action::SuggestedBounds(b) => { tracing::info!("Suggested bounds: {b:?}"); let core = self.app.core_mut(); core.applet.suggested_bounds = b; @@ -697,6 +791,40 @@ impl Cosmic { impl Cosmic { pub fn new(app: App) -> Self { - Self { app } + Self { + app, + #[cfg(feature = "wayland")] + surface_views: HashMap::new(), + } + } + + #[cfg(feature = "wayland")] + /// Create a subsurface + pub fn get_subsurface( + &mut self, + settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + >, + ) -> Task> { + use iced_winit::commands::subsurface::get_subsurface; + + self.surface_views.insert(settings.id, view); + get_subsurface(settings) + } + + #[cfg(feature = "wayland")] + /// Create a subsurface + pub fn get_popup( + &mut self, + settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + >, + ) -> Task> { + use iced_winit::commands::popup::get_popup; + + self.surface_views.insert(settings.id, view); + get_popup(settings) } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 940cc456..4af3f348 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,70 +6,27 @@ //! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application) //! example in our repository. -pub mod command; +mod action; +pub use action::Action; +use cosmic_config::CosmicConfigEntry; pub mod context_drawer; -mod core; pub mod cosmic; #[cfg(all(feature = "winit", feature = "multi-window"))] pub(crate) mod multi_window; pub mod settings; -pub mod message { - #[derive(Clone, Debug)] - #[must_use] - pub enum Message { - /// Messages from the application, for the application. - App(M), - /// Internal messages to be handled by libcosmic. - Cosmic(super::cosmic::Message), - #[cfg(feature = "single-instance")] - /// Dbus activation messages - DbusActivation(super::DbusActivationMessage), - /// Do nothing - None, - } +pub type Task = iced::Task>; - pub const fn app(message: M) -> Message { - Message::App(message) - } - - pub const fn cosmic(message: super::cosmic::Message) -> Message { - Message::Cosmic(message) - } - - pub const fn none() -> Message { - Message::None - } - - impl From for Message { - fn from(value: M) -> Self { - Self::App(value) - } - } -} - -use std::borrow::Cow; - -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::{container, horizontal_space, id_container, menu, nav_bar, popover}; +pub use crate::Core; use apply::Apply; use context_drawer::ContextDrawer; use iced::window; use iced::{Length, Subscription}; -pub use message::Message; -use url::Url; -#[cfg(feature = "single-instance")] -use { - iced_futures::futures::channel::mpsc::{Receiver, Sender}, - iced_futures::futures::SinkExt, - std::any::TypeId, - std::collections::HashMap, - zbus::{interface, proxy, zvariant::Value}, -}; +pub use settings::Settings; +use std::borrow::Cow; pub(crate) fn iced_settings( settings: Settings, @@ -143,6 +100,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res crate::malloc::limit_mmap_threshold(threshold); } + let default_font = settings.default_font; let (settings, mut flags, window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { @@ -179,142 +137,6 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res } } -#[cfg(feature = "single-instance")] -#[derive(Debug, Clone)] -pub struct DbusActivationMessage> { - pub activation_token: Option, - pub desktop_startup_id: Option, - pub msg: DbusActivationDetails, -} - -#[derive(Debug, Clone)] -pub enum DbusActivationDetails> { - Activate, - Open { - url: Vec, - }, - /// action can be deserialized as Flags - ActivateAction { - action: Action, - args: Args, - }, -} -#[cfg(feature = "single-instance")] -#[derive(Debug, Default)] -pub struct DbusActivation(Option>); -#[cfg(feature = "single-instance")] -impl DbusActivation { - #[must_use] - pub fn new() -> Self { - Self(None) - } - - pub fn rx(&mut self) -> Receiver { - let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10); - self.0 = Some(tx); - rx - } -} - -#[cfg(feature = "single-instance")] -#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)] -pub trait DbusActivationInterface { - /// Activate the application. - fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>; - - /// Open the given URIs. - fn open( - &mut self, - uris: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) -> zbus::Result<()>; - - /// Activate the given action. - fn activate_action( - &mut self, - action_name: &str, - parameter: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) -> zbus::Result<()>; -} - -#[cfg(feature = "single-instance")] -#[interface(name = "org.freedesktop.DbusActivation")] -impl DbusActivation { - async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(DbusActivationMessage { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: DbusActivationDetails::Activate, - }) - .await; - } - } - - async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(DbusActivationMessage { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: DbusActivationDetails::Open { - url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(), - }, - }) - .await; - } - } - - async fn activate_action( - &mut self, - action_name: &str, - parameter: Vec<&str>, - platform_data: HashMap<&str, Value<'_>>, - ) { - if let Some(tx) = &mut self.0 { - let _ = tx - .send(DbusActivationMessage { - activation_token: platform_data.get("activation-token").and_then(|t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }), - desktop_startup_id: platform_data.get("desktop-startup-id").and_then( - |t| match t { - Value::Str(t) => Some(t.to_string()), - _ => None, - }, - ), - msg: DbusActivationDetails::ActivateAction { - action: action_name.to_string(), - args: parameter - .iter() - .map(std::string::ToString::to_string) - .collect(), - }, - }) - .await; - } - } -} - #[cfg(feature = "single-instance")] /// Launch a COSMIC application with the given [`Settings`]. /// If the application is already running, the arguments will be passed to the @@ -326,6 +148,8 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { + use std::collections::HashMap; + let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); let override_single = std::env::var("COSMIC_SINGLE_INSTANCE") @@ -342,14 +166,14 @@ where return run::(settings, flags); }; - if DbusActivationInterfaceProxyBlocking::builder(&conn) + if crate::dbus_activation::DbusActivationInterfaceProxyBlocking::builder(&conn) .destination(App::APP_ID) .ok() .and_then(|b| b.path(path).ok()) .and_then(|b| b.destination(App::APP_ID).ok()) .and_then(|b| b.build().ok()) .is_some_and(|mut p| { - match { + let res = { let mut platform_data = HashMap::new(); if let Some(activation_token) = activation_token { platform_data.insert("activation-token", activation_token.into()); @@ -363,7 +187,8 @@ where } else { p.activate(platform_data) } - } { + }; + match res { Ok(()) => { tracing::info!("Successfully activated another instance"); true @@ -491,7 +316,7 @@ where } /// Allows overriding the default nav bar widget. - fn nav_bar(&self) -> Option>> { + fn nav_bar(&self) -> Option>> { if !self.core().nav_bar_active() { return None; } @@ -499,8 +324,8 @@ where let nav_model = self.nav_model()?; let mut nav = - crate::widget::nav_bar(nav_model, |id| Message::Cosmic(cosmic::Message::NavBar(id))) - .on_context(|id| Message::Cosmic(cosmic::Message::NavBarContext(id))) + crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id))) + .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) .context_menu(self.nav_context_menu(self.core().nav_bar_context())) .into_container() // XXX both must be shrink to avoid flex layout from ignoring it @@ -515,7 +340,10 @@ where } /// Shows a context menu for the active nav bar item. - fn nav_context_menu(&self, id: nav_bar::Id) -> Option>>> { + fn nav_context_menu( + &self, + id: nav_bar::Id, + ) -> Option>>> { None } @@ -605,7 +433,7 @@ where /// Handles dbus activation messages #[cfg(feature = "single-instance")] - fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Task { + fn dbus_activation(&mut self, msg: crate::dbus_activation::Message) -> Task { Task::none() } } @@ -648,7 +476,21 @@ pub trait ApplicationExt: Application { fn set_window_title(&mut self, title: String, id: window::Id) -> Task; /// View template for the main window. - fn view_main(&self) -> Element>; + fn view_main(&self) -> Element>; + + fn watch_config( + &self, + id: &'static str, + ) -> iced::Subscription> { + self.core().watch_config(id) + } + + fn watch_state( + &self, + id: &'static str, + ) -> iced::Subscription> { + self.core().watch_state(id) + } } impl ApplicationExt for App { @@ -695,7 +537,7 @@ impl ApplicationExt for App { #[allow(clippy::too_many_lines)] /// Creates the view for the main window. - fn view_main(&self) -> Element> { + fn view_main(&self) -> Element> { let core = self.core(); let is_condensed = core.is_condensed(); // TODO: More granularity might be needed for different resize border @@ -762,13 +604,13 @@ impl ApplicationExt for App { [0, 0, 0, 0] }) .apply(Element::from) - .map(Message::App), + .map(crate::Action::App), ); } else { //TODO: container and padding are temporary, until //the `resize_border` is moved to not cover window content widgets.push( - container(main_content.map(Message::App)) + container(main_content.map(crate::Action::App)) .padding(main_content_padding) .into(), ); @@ -778,7 +620,7 @@ impl ApplicationExt for App { //TODO: container and padding are temporary, until //the `resize_border` is moved to not cover window content widgets.push( - container(main_content.map(Message::App)) + container(main_content.map(crate::Action::App)) .padding(main_content_padding) .into(), ); @@ -794,7 +636,7 @@ impl ApplicationExt for App { context_width, ) .apply(Element::from) - .map(Message::App) + .map(crate::Action::App) .apply(container) .width(context_width) .apply(|drawer| { @@ -824,7 +666,7 @@ impl ApplicationExt for App { .push(content_row) .push_maybe( self.footer() - .map(|footer| container(footer.map(Message::App)).padding([0, 8, 8, 8])), + .map(|footer| container(footer.map(crate::Action::App)).padding([0, 8, 8, 8])), ); let content: Element<_> = if core.window.content_container { content_col @@ -851,45 +693,45 @@ impl ApplicationExt for App { let mut header = crate::widget::header_bar() .focused(focused) .title(&core.window.header_title) - .on_drag(Message::Cosmic(cosmic::Message::Drag)) - .on_right_click(Message::Cosmic(cosmic::Message::ShowWindowMenu)) - .on_double_click(Message::Cosmic(cosmic::Message::Maximize)); + .on_drag(crate::Action::Cosmic(Action::Drag)) + .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) + .on_double_click(crate::Action::Cosmic(Action::Maximize)); if self.nav_model().is_some() { let toggle = crate::widget::nav_bar_toggle() .active(core.nav_bar_active()) .selected(focused) .on_toggle(if is_condensed { - Message::Cosmic(cosmic::Message::ToggleNavBarCondensed) + crate::Action::Cosmic(Action::ToggleNavBarCondensed) } else { - Message::Cosmic(cosmic::Message::ToggleNavBar) + crate::Action::Cosmic(Action::ToggleNavBar) }); header = header.start(toggle); } if core.window.show_close { - header = header.on_close(Message::Cosmic(cosmic::Message::Close)); + header = header.on_close(crate::Action::Cosmic(Action::Close)); } if core.window.show_maximize && crate::config::show_maximize() { - header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize)); + header = header.on_maximize(crate::Action::Cosmic(Action::Maximize)); } if core.window.show_minimize && crate::config::show_minimize() { - header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize)); + header = header.on_minimize(crate::Action::Cosmic(Action::Minimize)); } for element in self.header_start() { - header = header.start(element.map(Message::App)); + header = header.start(element.map(crate::Action::App)); } for element in self.header_center() { - header = header.center(element.map(Message::App)); + header = header.center(element.map(crate::Action::App)); } for element in self.header_end() { - header = header.end(element.map(Message::App)); + header = header.end(element.map(crate::Action::App)); } if content_container { @@ -951,7 +793,7 @@ impl ApplicationExt for App { .dialog() .map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_dialog")))) { - popover = popover.popup(dialog.map(Message::App)); + popover = popover.popup(dialog.map(crate::Action::App)); } let view_element: Element<_> = popover.into(); @@ -959,74 +801,6 @@ impl ApplicationExt for App { } } -#[cfg(feature = "single-instance")] -fn single_instance_subscription() -> Subscription> { - use iced_futures::futures::StreamExt; - iced_futures::Subscription::run_with_id( - TypeId::of::(), - iced::stream::channel(10, move |mut output| async move { - let mut single_instance: DbusActivation = DbusActivation::new(); - let mut rx = single_instance.rx(); - if let Ok(builder) = zbus::ConnectionBuilder::session() { - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - if let Ok(conn) = builder.build().await { - // XXX Setup done this way seems to be more reliable. - // - // the docs for serve_at seem to imply it will replace the - // existing interface at the requested path, but it doesn't - // seem to work that way all the time. The docs for - // object_server().at() imply it won't replace the existing - // interface. - // - // request_name is used either way, with the builder or - // with the connection, but it must be done after the - // object server is setup. - if conn.object_server().at(path, single_instance).await != Ok(true) { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - if conn.request_name(App::APP_ID).await.is_err() { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - - #[cfg(feature = "smol")] - let handle = { - std::thread::spawn(move || { - let conn_clone = _conn.clone(); - - zbus::block_on(async move { - loop { - conn_clone.executor().tick().await; - } - }) - }) - }; - while let Some(mut msg) = rx.next().await { - if let Some(token) = msg.activation_token.take() { - if let Err(err) = output - .send(Message::Cosmic(cosmic::Message::Activate(token))) - .await - { - tracing::error!(?err, "Failed to send message"); - } - } - if let Err(err) = output.send(Message::DbusActivation(msg)).await { - tracing::error!(?err, "Failed to send message"); - } - } - } - } else { - tracing::warn!("Failed to connect to dbus for single instance"); - } - - loop { - iced::futures::pending!(); - } - }), - ) -} - const EMBEDDED_FONTS: &[&[u8]] = &[ include_bytes!("../../res/open-sans/OpenSans-Light.ttf"), include_bytes!("../../res/open-sans/OpenSans-Regular.ttf"), @@ -1043,6 +817,6 @@ fn preload_fonts() { .unwrap(); EMBEDDED_FONTS - .into_iter() + .iter() .for_each(move |font| font_system.load_font(Cow::Borrowed(font))); } diff --git a/src/app/multi_window.rs b/src/app/multi_window.rs index d20739b1..368f2f03 100644 --- a/src/app/multi_window.rs +++ b/src/app/multi_window.rs @@ -1,3 +1,6 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + //! Create and run daemons that run in the background. //! Copied from iced 0.13, but adds optional initial window diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0ca8a4f1..25af302a 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -2,7 +2,7 @@ pub mod token; use crate::{ - app::{self, iced_settings, Core}, + app::iced_settings, cctk::sctk, iced::{ self, @@ -14,20 +14,17 @@ use crate::{ theme::{self, system_dark, system_light, Button, THEME}, widget::{ self, - autosize::{autosize, Autosize}, + autosize::{self, autosize, Autosize}, layer_container, }, Application, Element, Renderer, }; -use cctk::sctk::shell::xdg::window::WindowConfigure; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; -use cosmic_theme::Theme; -use iced::Pixels; -use iced_core::{Padding, Shadow}; +use iced_core::{Layout, Padding, Shadow}; 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, sync::LazyLock}; +use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; use crate::app::cosmic; @@ -35,6 +32,8 @@ 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")); +static TOOLTIP_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("subsurface")); +static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); #[derive(Debug, Clone)] pub struct Context { @@ -161,11 +160,7 @@ impl Context { let height = f32::from(height) + applet_padding as f32 * 2.; let mut settings = crate::app::Settings::default() .size(iced_core::Size::new(width, height)) - .size_limits( - Limits::NONE - .min_height(height as f32) - .min_width(width as f32), - ) + .size_limits(Limits::NONE.min_height(height).min_width(width)) .resizable(None) .default_text_size(14.0) .default_font(crate::font::default()) @@ -224,6 +219,70 @@ impl Context { ) } + pub fn applet_tooltip<'a, Message: 'static>( + &self, + content: impl Into>, + tooltip: impl Into>, + has_popup: bool, + on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, + ) -> crate::widget::wayland::tooltip::widget::Tooltip<'a, Message, Message> { + let window_id = *TOOLTIP_WINDOW_ID; + let subsurface_id = TOOLTIP_ID.clone(); + let anchor = self.anchor; + let tooltip = tooltip.into(); + + crate::widget::wayland::tooltip::widget::Tooltip::<'a, Message, Message>::new( + content, + (!has_popup).then_some(move |bounds: Rectangle| { + let window_id = window_id; + let (popup_anchor, gravity) = match anchor { + PanelAnchor::Left => (Anchor::Right, Gravity::Right), + PanelAnchor::Right => (Anchor::Left, Gravity::Left), + PanelAnchor::Top => (Anchor::Bottom, Gravity::Bottom), + PanelAnchor::Bottom => (Anchor::Top, Gravity::Top), + }; + + SctkPopupSettings { + parent: window::Id::RESERVED, + id: window_id, + grab: false, + input_zone: Some(Rectangle::new( + iced::Point::new(-1000., -1000.), + iced::Size::default(), + )), + positioner: SctkPositioner { + size: None, + size_limits: Limits::NONE.min_width(1.).min_height(1.), + anchor_rect: Rectangle { + x: bounds.x.round() as i32, + y: bounds.y.round() as i32, + width: bounds.width.round() as i32, + height: bounds.height.round() as i32, + }, + anchor: popup_anchor, + gravity, + constraint_adjustment: 15, + offset: (0, 0), + reactive: true, + }, + parent_size: None, + close_with_children: true, + } + }), + move || { + Element::from(autosize::autosize( + layer_container(crate::widget::text(tooltip.clone())) + .layer(crate::cosmic_theme::Layer::Background) + .padding(4.), + subsurface_id.clone(), + )) + }, + on_surface_action(crate::surface::Action::DestroyPopup(window_id)), + on_surface_action, + ) + .delay(Duration::from_millis(100)) + } + // TODO popup container which tracks the size of itself and requests the popup to resize to match pub fn popup_container<'a, Message: 'static>( &self, @@ -240,7 +299,7 @@ impl Context { Container::::new( Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); - let corners = cosmic.corner_radii.clone(); + let corners = cosmic.corner_radii; iced_widget::container::Style { text_color: Some(cosmic.background.on.into()), background: Some(Color::from(cosmic.background.base).into()), @@ -262,10 +321,10 @@ impl Context { ) .limits( Limits::NONE - .min_width(1.) .min_height(1.) - .max_width(500.) - .max_height(1000.), + .min_width(360.0) + .max_width(360.0) + .max_height(1000.0), ) } @@ -304,10 +363,16 @@ impl Context { }, reactive: true, constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y - ..Default::default() + size_limits: Limits::NONE + .min_height(1.0) + .min_width(360.0) + .max_width(360.0) + .max_height(1080.0), }, parent_size: None, grab: true, + close_with_children: false, + input_zone: None, } } @@ -315,7 +380,7 @@ impl Context { &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 force_configured = matches!(&self.panel_type, PanelType::Other(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(); @@ -326,7 +391,7 @@ impl Context { .filter(|c| c.width as i32 > 0) .map(|c| c.width) { - limits = limits.width(width as f32); + limits = limits.width(width); } if let Some(height) = self .suggested_bounds @@ -334,7 +399,7 @@ impl Context { .filter(|c| c.height as i32 > 0) .map(|c| c.height) { - limits = limits.height(height as f32); + limits = limits.height(height); } w.limits(limits) diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 00000000..73c900c1 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,45 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced::window; + +/// Initiates a window drag. +pub fn drag(id: window::Id) -> iced::Task> { + iced_runtime::window::drag(id) +} + +/// Maximizes the window. +pub fn maximize(id: window::Id, maximized: bool) -> iced::Task> { + iced_runtime::window::maximize(id, maximized) +} + +/// Minimizes the window. +pub fn minimize(id: window::Id) -> iced::Task> { + iced_runtime::window::minimize(id, true) +} + +/// Sets the title of a window. +#[allow(unused_variables, clippy::needless_pass_by_value)] +pub fn set_title(id: window::Id, title: String) -> iced::Task> { + iced::Task::none() +} + +#[cfg(feature = "winit")] +pub fn set_scaling_factor(factor: f32) -> iced::Task> { + iced::Task::done(crate::app::Action::ScaleFactor(factor)).map(crate::Action::Cosmic) +} + +#[cfg(feature = "winit")] +pub fn set_theme(theme: crate::Theme) -> iced::Task> { + iced::Task::done(crate::app::Action::AppThemeChange(theme)).map(crate::Action::Cosmic) +} + +/// Sets the window mode to windowed. +pub fn set_windowed(id: window::Id) -> iced::Task> { + iced_runtime::window::change_mode(id, window::Mode::Windowed) +} + +/// Toggles the windows' maximize state. +pub fn toggle_maximize(id: window::Id) -> iced::Task> { + iced_runtime::window::toggle_maximize(id) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 4f1a5a16..1e82becd 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -17,7 +17,7 @@ const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono"; const SANS_FAMILY_DEFAULT: &str = "Open Sans"; /// Stores static strings of the family names for `iced::Font` compatibility. -pub static FAMILY_MAP: LazyLock>> = LazyLock::new(|| Mutex::default()); +pub static FAMILY_MAP: LazyLock>> = LazyLock::new(Mutex::default); pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { RwLock::new( @@ -153,7 +153,7 @@ impl From for iced::Font { let name: &'static str = family_map .get(font.family.as_str()) - .map(|&x| x) + .copied() .unwrap_or_else(|| { let value = font.family.clone().leak(); family_map.insert(value); diff --git a/src/app/core.rs b/src/core.rs similarity index 84% rename from src/app/core.rs rename to src/core.rs index 42c4429f..fdeeba5b 100644 --- a/src/app/core.rs +++ b/src/core.rs @@ -1,12 +1,12 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use std::{cell::OnceCell, collections::HashMap}; +use std::collections::HashMap; use crate::widget::nav_bar; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; -use iced::window; +use iced::{window, Limits, Size}; use iced_core::window::Id; use palette::Srgba; use slotmap::Key; @@ -95,6 +95,8 @@ pub struct Core { pub(crate) main_window: Option, pub(crate) exit_on_main_window_closed: bool, + + pub(crate) menu_bars: HashMap, } impl Default for Core { @@ -151,6 +153,7 @@ impl Default for Core { portal_is_high_contrast: None, main_window: None, exit_on_main_window_closed: true, + menu_bars: HashMap::new(), } } } @@ -205,7 +208,7 @@ impl Core { // Context drawer min width (344px) + padding (8px) breakpoint += 344.0 + 8.0; }; - self.is_condensed = (breakpoint * self.scale_factor) > self.window.width as f32; + self.is_condensed = (breakpoint * self.scale_factor) > self.window.width; self.nav_bar_update(); } @@ -218,7 +221,7 @@ impl Core { } pub(crate) fn context_width(&self, has_nav: bool) -> f32 { - let window_width = (self.window.width as f32) / self.scale_factor; + let window_width = self.window.width / self.scale_factor; // Content width (360px) + padding (8px) let mut reserved_width = 360.0 + 8.0; @@ -243,6 +246,10 @@ impl Core { } } + pub fn main_window_is(&self, id: iced::window::Id) -> bool { + self.main_window_id().is_some_and(|main_id| main_id == id) + } + /// Whether the nav panel is visible or not #[must_use] pub fn nav_bar_active(&self) -> bool { @@ -353,7 +360,7 @@ impl Core { /// Get the current focused window if it exists #[must_use] pub fn focused_window(&self) -> Option { - self.focused_window.clone() + self.focused_window } /// Whether the application should use a dark theme, according to the system @@ -374,4 +381,64 @@ impl Core { std::mem::swap(&mut self.main_window, &mut id); id } + + #[cfg(feature = "winit")] + pub fn drag(&self, id: Option) -> crate::app::Task { + let Some(id) = id.or(self.main_window) else { + return iced::Task::none(); + }; + crate::command::drag(id) + } + + #[cfg(feature = "winit")] + pub fn maximize( + &self, + id: Option, + maximized: bool, + ) -> crate::app::Task { + let Some(id) = id.or(self.main_window) else { + return iced::Task::none(); + }; + crate::command::maximize(id, maximized) + } + + #[cfg(feature = "winit")] + pub fn minimize(&self, id: Option) -> crate::app::Task { + let Some(id) = id.or(self.main_window) else { + return iced::Task::none(); + }; + crate::command::minimize(id) + } + + #[cfg(feature = "winit")] + pub fn set_title( + &self, + id: Option, + title: String, + ) -> crate::app::Task { + let Some(id) = id.or(self.main_window) else { + return iced::Task::none(); + }; + crate::command::set_title(id, title) + } + + #[cfg(feature = "winit")] + pub fn set_windowed(&self, id: Option) -> crate::app::Task { + let Some(id) = id.or(self.main_window) else { + return iced::Task::none(); + }; + crate::command::set_windowed(id) + } + + #[cfg(feature = "winit")] + pub fn toggle_maximize( + &self, + id: Option, + ) -> crate::app::Task { + let Some(id) = id.or(self.main_window) else { + return iced::Task::none(); + }; + + crate::command::toggle_maximize(id) + } } diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs new file mode 100644 index 00000000..84b5d000 --- /dev/null +++ b/src/dbus_activation.rs @@ -0,0 +1,214 @@ +// Copyright 2024 System76 +// SPDX-License-Identifier: MPL-2.0 + +use { + crate::ApplicationExt, + iced::Subscription, + iced_futures::futures::{ + channel::mpsc::{Receiver, Sender}, + SinkExt, + }, + std::{any::TypeId, collections::HashMap}, + url::Url, + zbus::{interface, proxy, zvariant::Value}, +}; + +pub fn subscription() -> Subscription> { + use iced_futures::futures::StreamExt; + iced_futures::Subscription::run_with_id( + TypeId::of::(), + iced::stream::channel(10, move |mut output| async move { + let mut single_instance: DbusActivation = DbusActivation::new(); + let mut rx = single_instance.rx(); + if let Ok(builder) = zbus::ConnectionBuilder::session() { + let path: String = format!("/{}", App::APP_ID.replace('.', "/")); + if let Ok(conn) = builder.build().await { + // XXX Setup done this way seems to be more reliable. + // + // the docs for serve_at seem to imply it will replace the + // existing interface at the requested path, but it doesn't + // seem to work that way all the time. The docs for + // object_server().at() imply it won't replace the existing + // interface. + // + // request_name is used either way, with the builder or + // with the connection, but it must be done after the + // object server is setup. + if conn.object_server().at(path, single_instance).await != Ok(true) { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + if conn.request_name(App::APP_ID).await.is_err() { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + + #[cfg(feature = "smol")] + let handle = { + std::thread::spawn(move || { + let conn_clone = _conn.clone(); + + zbus::block_on(async move { + loop { + conn_clone.executor().tick().await; + } + }) + }) + }; + while let Some(mut msg) = rx.next().await { + if let Some(token) = msg.activation_token.take() { + if let Err(err) = output + .send(crate::Action::Cosmic(crate::app::Action::Activate(token))) + .await + { + tracing::error!(?err, "Failed to send message"); + } + } + if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { + tracing::error!(?err, "Failed to send message"); + } + } + } + } else { + tracing::warn!("Failed to connect to dbus for single instance"); + } + + loop { + iced::futures::pending!(); + } + }), + ) +} + +#[derive(Debug, Clone)] +pub struct Message> { + pub activation_token: Option, + pub desktop_startup_id: Option, + pub msg: Details, +} + +#[derive(Debug, Clone)] +pub enum Details> { + Activate, + Open { + url: Vec, + }, + /// action can be deserialized as Flags + ActivateAction { + action: Action, + args: Args, + }, +} + +#[derive(Debug, Default)] +pub struct DbusActivation(Option>); + +impl DbusActivation { + #[must_use] + pub fn new() -> Self { + Self(None) + } + + pub fn rx(&mut self) -> Receiver { + let (tx, rx) = iced_futures::futures::channel::mpsc::channel(10); + self.0 = Some(tx); + rx + } +} + +#[proxy(interface = "org.freedesktop.DbusActivation", assume_defaults = true)] +pub trait DbusActivationInterface { + /// Activate the application. + fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) -> zbus::Result<()>; + + /// Open the given URIs. + fn open( + &mut self, + uris: Vec<&str>, + platform_data: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; + + /// Activate the given action. + fn activate_action( + &mut self, + action_name: &str, + parameter: Vec<&str>, + platform_data: HashMap<&str, Value<'_>>, + ) -> zbus::Result<()>; +} + +#[interface(name = "org.freedesktop.DbusActivation")] +impl DbusActivation { + async fn activate(&mut self, platform_data: HashMap<&str, Value<'_>>) { + if let Some(tx) = &mut self.0 { + let _ = tx + .send(Message { + activation_token: platform_data.get("activation-token").and_then(|t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }), + desktop_startup_id: platform_data.get("desktop-startup-id").and_then( + |t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }, + ), + msg: Details::Activate, + }) + .await; + } + } + + async fn open(&mut self, uris: Vec<&str>, platform_data: HashMap<&str, Value<'_>>) { + if let Some(tx) = &mut self.0 { + let _ = tx + .send(Message { + activation_token: platform_data.get("activation-token").and_then(|t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }), + desktop_startup_id: platform_data.get("desktop-startup-id").and_then( + |t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }, + ), + msg: Details::Open { + url: uris.iter().filter_map(|u| Url::parse(u).ok()).collect(), + }, + }) + .await; + } + } + + async fn activate_action( + &mut self, + action_name: &str, + parameter: Vec<&str>, + platform_data: HashMap<&str, Value<'_>>, + ) { + if let Some(tx) = &mut self.0 { + let _ = tx + .send(Message { + activation_token: platform_data.get("activation-token").and_then(|t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }), + desktop_startup_id: platform_data.get("desktop-startup-id").and_then( + |t| match t { + Value::Str(t) => Some(t.to_string()), + _ => None, + }, + ), + msg: Details::ActivateAction { + action: action_name.to_string(), + args: parameter + .iter() + .map(std::string::ToString::to_string) + .collect(), + }, + }) + .await; + } + } +} diff --git a/src/desktop.rs b/src/desktop.rs index 1bee3189..3f5f5755 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -74,10 +74,7 @@ pub fn load_applications<'a>( #[cfg(not(windows))] pub fn app_id_or_fallback_matches(app_id: &str, entry: &DesktopEntryData) -> bool { - let lowercase_wm_class = match entry.wm_class.as_ref() { - Some(s) => Some(s.to_lowercase()), - None => None, - }; + let lowercase_wm_class = entry.wm_class.as_ref().map(|s| s.to_lowercase()); app_id == entry.id || Some(app_id.to_lowercase()) == lowercase_wm_class diff --git a/src/ext.rs b/src/ext.rs index 215f7b7b..c85e6e86 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -9,7 +9,7 @@ pub trait ElementExt { fn debug(self, debug: bool) -> Self; } -impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> { +impl ElementExt for crate::Element<'_, Message> { fn debug(self, debug: bool) -> Self { if debug { self.explain(Color::WHITE) diff --git a/src/keyboard_nav.rs b/src/keyboard_nav.rs index cbfbf4a5..65211462 100644 --- a/src/keyboard_nav.rs +++ b/src/keyboard_nav.rs @@ -8,7 +8,7 @@ use iced_core::keyboard::key::Named; use iced_futures::event::listen_raw; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Message { +pub enum Action { Escape, FocusNext, FocusPrevious, @@ -16,7 +16,7 @@ pub enum Message { Search, } -pub fn subscription() -> Subscription { +pub fn subscription() -> Subscription { listen_raw(|event, status, _| { if event::Status::Ignored != status { return None; @@ -30,18 +30,18 @@ pub fn subscription() -> Subscription { }) => match key { Named::Tab if !modifiers.control() => { return Some(if modifiers.shift() { - Message::FocusPrevious + Action::FocusPrevious } else { - Message::FocusNext + Action::FocusNext }); } Named::Escape => { - return Some(Message::Escape); + return Some(Action::Escape); } Named::F11 => { - return Some(Message::Fullscreen); + return Some(Action::Fullscreen); } _ => (), @@ -51,7 +51,7 @@ pub fn subscription() -> Subscription { modifiers, .. }) if c == "f" && modifiers.control() => { - return Some(Message::Search); + return Some(Action::Search); } _ => (), diff --git a/src/lib.rs b/src/lib.rs index bd4651ac..0515911d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,21 +9,30 @@ pub mod prelude { pub use crate::ext::*; #[cfg(feature = "winit")] pub use crate::ApplicationExt; - pub use crate::{Also, Apply, Element, Renderer, Theme}; + pub use crate::{Also, Apply, Element, Renderer, Task, Theme}; } pub use apply::{Also, Apply}; +/// Actions are managed internally by the cosmic runtime. +pub mod action; +pub use action::Action; + #[cfg(feature = "winit")] pub mod app; #[cfg(feature = "winit")] +#[doc(inline)] pub use app::{Application, ApplicationExt}; #[cfg(feature = "applet")] pub mod applet; -pub use iced::Task; -pub mod task; +pub mod command; + +/// State which is managed by the cosmic runtime. +pub mod core; +#[doc(inline)] +pub use core::Core; pub mod config; @@ -33,6 +42,11 @@ pub use cosmic_config; #[doc(inline)] pub use cosmic_theme; +#[cfg(feature = "single-instance")] +pub mod dbus_activation; +#[cfg(feature = "single-instance")] +pub use dbus_activation::DbusActivation; + #[cfg(feature = "desktop")] pub mod desktop; @@ -85,6 +99,11 @@ pub mod process; #[cfg(feature = "wayland")] pub use cctk; +pub mod surface; + +pub use iced::Task; +pub mod task; + pub mod theme; #[doc(inline)] diff --git a/src/malloc.rs b/src/malloc.rs index e980ea6f..0d271447 100644 --- a/src/malloc.rs +++ b/src/malloc.rs @@ -1,3 +1,6 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + use std::os::raw::c_int; const M_MMAP_THRESHOLD: c_int = -3; diff --git a/src/process.rs b/src/process.rs index 037bed1e..f76dad7e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,6 +1,8 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + #[cfg(all(feature = "smol", not(feature = "tokio")))] use smol::io::AsyncReadExt; -use std::fs::File; use std::io; use std::os::fd::OwnedFd; use std::process::{exit, Command, Stdio}; diff --git a/src/surface/action.rs b/src/surface/action.rs new file mode 100644 index 00000000..af7cc2de --- /dev/null +++ b/src/surface/action.rs @@ -0,0 +1,152 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +use super::Action; +#[cfg(feature = "winit")] +use crate::Application; + +use std::{any::Any, sync::Arc}; + +/// Used to produce a destroy popup message from within a widget. +#[cfg(feature = "wayland")] +#[must_use] +pub fn destroy_popup(id: iced_core::window::Id) -> Action { + Action::DestroyPopup(id) +} + +#[cfg(feature = "wayland")] +#[must_use] +pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { + Action::DestroySubsurface(id) +} + +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn app_popup( + settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, +) -> Action { + let boxed: Box< + dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::AppPopup( + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) +} + +/// Used to create a subsurface message from within a widget. +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn simple_subsurface( + settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + + Send + + Sync + + 'static, + view: Option< + Box crate::Element<'static, crate::Action> + Send + Sync + 'static>, + >, +) -> Action { + let boxed: Box< + dyn Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::Subsurface( + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) +} + +/// Used to create a popup message from within a widget. +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn simple_popup( + settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + view: Option< + impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +) -> Action { + let boxed: Box< + dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::Popup( + Arc::new(boxed), + view.map(|view| { + let boxed: Box< + dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) +} + +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn subsurface( + settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + + Send + + Sync + + 'static, + // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, +) -> Action { + let boxed: Box< + dyn Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::AppSubsurface( + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) +} diff --git a/src/surface/mod.rs b/src/surface/mod.rs new file mode 100644 index 00000000..c08108ee --- /dev/null +++ b/src/surface/mod.rs @@ -0,0 +1,85 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +pub mod action; + +use iced::Limits; +use iced::Size; +use iced::Task; +use std::future::Future; +use std::sync::Arc; + +/// Ignore this message in your application. It will be intercepted. +#[derive(Clone)] +pub enum Action { + /// Create a subsurface with a view function accepting the App as a parameter + AppSubsurface( + std::sync::Arc>, + Option>>, + ), + /// Create a subsurface with a view function + Subsurface( + std::sync::Arc>, + Option>>, + ), + /// Destroy a subsurface with a view function + DestroySubsurface(iced::window::Id), + /// Create a popup with a view function accepting the App as a parameter + AppPopup( + std::sync::Arc>, + Option>>, + ), + /// Create a popup + Popup( + std::sync::Arc>, + Option>>, + ), + /// Destroy a subsurface with a view function + DestroyPopup(iced::window::Id), + /// Responsive menu bar update + ResponsiveMenuBar { + /// Id of the menu bar + menu_bar: crate::widget::Id, + /// Limits of the menu bar + limits: Limits, + /// Requested Full Size for expanded menu bar + size: Size, + }, + Ignore, + Task(Arc Task + Send + Sync>), +} + +impl std::fmt::Debug for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AppSubsurface(arg0, arg1) => f + .debug_tuple("AppSubsurface") + .field(arg0) + .field(arg1) + .finish(), + Self::Subsurface(arg0, arg1) => { + f.debug_tuple("Subsurface").field(arg0).field(arg1).finish() + } + Self::DestroySubsurface(arg0) => { + f.debug_tuple("DestroySubsurface").field(arg0).finish() + } + Self::AppPopup(arg0, arg1) => { + f.debug_tuple("AppPopup").field(arg0).field(arg1).finish() + } + Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), + Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), + Self::ResponsiveMenuBar { + menu_bar, + limits, + size, + } => f + .debug_struct("ResponsiveMenuBar") + .field("menu_bar", menu_bar) + .field("limits", limits) + .field("size", size) + .finish(), + Self::Ignore => write!(f, "Ignore"), + Self::Task(_) => f.debug_tuple("Future").finish(), + } + } +} diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 00000000..a730b37e --- /dev/null +++ b/src/task.rs @@ -0,0 +1,25 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Create asynchronous actions to be performed in the background. + +use std::future::Future; + +/// Yields a task which contains a batch of tasks. +pub fn batch, Y: Send + 'static>( + tasks: impl IntoIterator>, +) -> iced::Task { + iced::Task::batch(tasks).map(Into::into) +} + +/// Yields a task which will run the future on the runtime executor. +pub fn future, Y: 'static>( + future: impl Future + Send + 'static, +) -> iced::Task { + iced::Task::future(async move { future.await.into() }) +} + +/// Yields a task which will return a message. +pub fn message, Y: 'static>(message: X) -> iced::Task { + future(async move { message.into() }) +} diff --git a/src/task/mod.rs b/src/task/mod.rs deleted file mode 100644 index 379ea19b..00000000 --- a/src/task/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Create asynchronous actions to be performed in the background. - -use iced::window; -use iced::Task; -use iced_core::window::Mode; -use iced_runtime::{task, Action}; -use std::future::Future; - -/// Yields a task which contains a batch of tasks. -pub fn batch, Y: Send + 'static>( - tasks: impl IntoIterator>, -) -> Task { - Task::batch(tasks).map(Into::into) -} - -/// Yields a task which will run the future on the runtime executor. -pub fn future, Y: 'static>(future: impl Future + Send + 'static) -> Task { - Task::future(async move { future.await.into() }) -} - -/// Yields a task which will return a message. -pub fn message, Y: 'static>(message: X) -> Task { - future(async move { message.into() }) -} - -/// Initiates a window drag. -pub fn drag(id: window::Id) -> Task { - iced_runtime::window::drag(id) -} - -/// Maximizes the window. -pub fn maximize(id: window::Id, maximized: bool) -> Task { - iced_runtime::window::maximize(id, maximized) -} - -/// Minimizes the window. -pub fn minimize(id: window::Id) -> Task { - iced_runtime::window::minimize(id, true) -} - -/// Sets the title of a window. -#[allow(unused_variables, clippy::needless_pass_by_value)] -pub fn set_title(id: window::Id, title: String) -> Task { - Task::none() -} - -/// Sets the window mode to windowed. -pub fn set_windowed(id: window::Id) -> Task { - iced_runtime::window::change_mode(id, Mode::Windowed) -} - -/// Toggles the windows' maximize state. -pub fn toggle_maximize(id: window::Id) -> Task { - iced_runtime::window::toggle_maximize(id) -} diff --git a/src/theme/portal.rs b/src/theme/portal.rs index 17225ccc..e3dc7511 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::{stream, subscription}; +use iced_futures::stream; use tracing::error; #[derive(Debug, Clone)] @@ -27,7 +27,7 @@ pub fn desktop_settings() -> iced_futures::Subscription { .await; #[cfg(not(feature = "tokio"))] { - pending::<()>().await; + futures::future::pending::<()>().await; unreachable!(); } attempts += 1; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 44af94b1..2e5d8c13 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -756,9 +756,7 @@ impl slider::Catalog for Theme { impl menu::Catalog for Theme { type Class<'a> = (); - fn default<'a>() -> ::Class<'a> { - () - } + fn default<'a>() -> ::Class<'a> {} fn style(&self, class: &::Class<'_>) -> menu::Style { let cosmic = self.cosmic(); @@ -779,9 +777,7 @@ impl menu::Catalog for Theme { impl pick_list::Catalog for Theme { type Class<'a> = (); - fn default<'a>() -> ::Class<'a> { - () - } + fn default<'a>() -> ::Class<'a> {} fn style( &self, @@ -824,9 +820,7 @@ impl pick_list::Catalog for Theme { impl radio::Catalog for Theme { type Class<'a> = (); - fn default<'a>() -> Self::Class<'a> { - () - } + fn default<'a>() -> Self::Class<'a> {} fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let theme = self.cosmic(); @@ -878,9 +872,7 @@ impl radio::Catalog for Theme { impl toggler::Catalog for Theme { type Class<'a> = (); - fn default<'a>() -> Self::Class<'a> { - () - } + fn default<'a>() -> Self::Class<'a> {} fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { let cosmic = self.cosmic(); @@ -935,9 +927,7 @@ impl toggler::Catalog for Theme { impl pane_grid::Catalog for Theme { type Class<'a> = (); - fn default<'a>() -> ::Class<'a> { - () - } + fn default<'a>() -> ::Class<'a> {} fn style(&self, class: &::Class<'_>) -> pane_grid::Style { let theme = self.cosmic(); diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 5acb0d09..7f99a1a5 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -1,6 +1,8 @@ // From iced_aw, license MIT //! Change the appearance of menu bars and their menus. +use std::sync::Arc; + use crate::Theme; use iced_widget::core::Color; @@ -33,19 +35,19 @@ pub trait StyleSheet { } /// The style of a menu bar and its menus -#[derive(Default)] +#[derive(Default, Clone)] #[allow(missing_debug_implementations)] pub enum MenuBarStyle { /// The default style. #[default] Default, /// A [`Theme`] that uses a `Custom` palette. - Custom(Box>), + Custom(Arc>), } impl From Appearance> for MenuBarStyle { fn from(f: fn(&Theme) -> Appearance) -> Self { - Self::Custom(Box::new(f)) + Self::Custom(Arc::new(f)) } } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index 1cbd4ef5..a187374c 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -31,3 +31,8 @@ pub use self::segmented_button::SegmentedButton; mod text_input; #[doc(inline)] pub use self::text_input::TextInput; + +#[cfg(all(feature = "wayland", feature = "winit"))] +pub mod tooltip; +#[cfg(all(feature = "wayland", feature = "winit"))] +pub use tooltip::Tooltip; diff --git a/src/theme/style/tooltip.rs b/src/theme/style/tooltip.rs new file mode 100644 index 00000000..a0564e63 --- /dev/null +++ b/src/theme/style/tooltip.rs @@ -0,0 +1,31 @@ +use iced::Color; + +use crate::widget::wayland::tooltip::Catalog; + +#[derive(Default)] +pub enum Tooltip { + #[default] + Default, +} + +impl Catalog for crate::Theme { + type Class = Tooltip; + + fn style(&self, style: &Self::Class) -> crate::widget::wayland::tooltip::Style { + let cosmic = self.cosmic(); + + match style { + Tooltip::Default => crate::widget::wayland::tooltip::Style { + text_color: cosmic.on_bg_color().into(), + background: None, + border_width: 0.0, + border_radius: cosmic.corner_radii.radius_0.into(), + border_color: Color::TRANSPARENT, + shadow_offset: iced::Vector::default(), + outline_width: Default::default(), + outline_color: Color::TRANSPARENT, + icon_color: None, + }, + } + } +} diff --git a/src/widget/about.rs b/src/widget/about.rs index 5026d410..acd13ac9 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -183,7 +183,7 @@ pub fn about<'a, Message: Clone + 'static>( .align_y(Alignment::Center), ) .class(crate::theme::Button::Text) - .on_press(on_url_press(url.unwrap_or(String::new()))) + .on_press(on_url_press(url.unwrap_or_default())) .width(Length::Fill), ) }); diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index ec8e2bed..39ac9c57 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -12,7 +12,6 @@ use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, }; -use iced_widget::container; pub use iced_widget::container::{Catalog, Style}; pub fn aspect_ratio_container<'a, Message: 'static, T>( @@ -35,7 +34,7 @@ where container: Container<'a, Message, crate::Theme, Renderer>, } -impl<'a, Message, Renderer> AspectRatio<'a, Message, Renderer> +impl AspectRatio<'_, Message, Renderer> where Renderer: iced_core::Renderer, { @@ -146,8 +145,8 @@ where } } -impl<'a, Message, Renderer> Widget - for AspectRatio<'a, Message, Renderer> +impl Widget + for AspectRatio<'_, Message, Renderer> where Renderer: iced_core::Renderer, { diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index adef4490..6c15750d 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -90,8 +90,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget - for Autosize<'a, Message, Theme, Renderer> +impl Widget + for Autosize<'_, Message, Theme, Renderer> where Renderer: iced_core::Renderer, { diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 0322d58c..3b46d9d1 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -29,7 +29,7 @@ pub fn icon<'a, Message>(handle: impl Into) -> Button<'a, Message> { }) } -impl<'a, Message> Button<'a, Message> { +impl Button<'_, Message> { pub fn new(icon: Icon) -> Self { let guard = crate::theme::THEME.lock().unwrap(); let theme = guard.cosmic(); diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index a0508aaf..74e3b378 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Builder, Style}; +use super::Builder; use crate::{ widget::{self, image::Handle}, Element, diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index 6bf6338a..9928628b 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -111,7 +111,7 @@ pub struct Builder<'a, Message, Variant> { variant: Variant, } -impl<'a, Message, Variant> Builder<'a, Message, Variant> { +impl Builder<'_, Message, Variant> { /// Set the value of [`on_press`] as either `Some` or `None`. pub fn on_press_maybe(mut self, on_press: Option) -> Self { self.on_press = on_press; diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 2070fb16..1b682393 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, ButtonClass, Style}; +use super::{Builder, ButtonClass}; use crate::widget::{icon, row, tooltip}; use crate::{ext::CollectionWidget, Element}; use apply::Apply; @@ -42,6 +42,12 @@ pub struct Text { pub(super) trailing_icon: Option, } +impl Default for Text { + fn default() -> Self { + Self::new() + } +} + impl Text { pub const fn new() -> Self { Self { @@ -51,7 +57,7 @@ impl Text { } } -impl<'a, Message> Button<'a, Message> { +impl Button<'_, Message> { pub fn new(text: Text) -> Self { let guard = crate::theme::THEME.lock().unwrap(); let theme = guard.cosmic(); diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index a51adbb5..c3e51495 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -55,6 +55,7 @@ pub struct Button<'a, Message> { selected: bool, style: crate::theme::Button, variant: Variant, + force_enabled: bool, } impl<'a, Message> Button<'a, Message> { @@ -77,6 +78,7 @@ impl<'a, Message> Button<'a, Message> { selected: false, style: crate::theme::Button::default(), variant: Variant::Normal, + force_enabled: false, } } @@ -90,6 +92,7 @@ impl<'a, Message> Button<'a, Message> { name: None, #[cfg(feature = "a11y")] description: None, + force_enabled: false, #[cfg(feature = "a11y")] label: None, content: content.into(), @@ -163,6 +166,12 @@ impl<'a, Message> Button<'a, Message> { self } + /// Sets the the [`Button`] to enabled whether or not it has handlers for on press. + pub fn force_enabled(mut self, enabled: bool) -> Self { + self.force_enabled = enabled; + self + } + /// Sets the widget to a selected state. /// /// Displays a selection indicator on image buttons. @@ -348,7 +357,8 @@ impl<'a, Message: 'a + Clone> Widget let mut headerbar_alpha = None; - let is_enabled = self.on_press.is_some() || self.on_press_down.is_some(); + let is_enabled = + self.on_press.is_some() || self.on_press_down.is_some() || self.force_enabled; let is_mouse_over = cursor.position().is_some_and(|p| bounds.contains(p)); let state = tree.state.downcast_ref::(); @@ -583,12 +593,7 @@ impl<'a, Message: 'a + Clone> Widget } match self.description.as_ref() { Some(iced_accessibility::Description::Id(id)) => { - node.set_described_by( - id.iter() - .cloned() - .map(|id| NodeId::from(id)) - .collect::>(), - ); + node.set_described_by(id.iter().cloned().map(NodeId::from).collect::>()); } Some(iced_accessibility::Description::Text(text)) => { node.set_description(text.clone()); diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index b1ee927f..ea96360e 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -53,7 +53,7 @@ impl CalendarModel { let now = Local::now(); let naive_now = NaiveDate::from(now.naive_local()); CalendarModel { - selected: naive_now.clone(), + selected: naive_now, visible: naive_now, } } @@ -65,36 +65,34 @@ impl CalendarModel { pub fn show_prev_month(&mut self) { let prev_month_date = self .visible - .clone() .checked_sub_months(Months::new(1)) .expect("valid naivedate"); - self.visible = prev_month_date.clone(); + self.visible = prev_month_date; } pub fn show_next_month(&mut self) { let next_month_date = self .visible - .clone() .checked_add_months(Months::new(1)) .expect("valid naivedate"); - self.visible = next_month_date.clone(); + self.visible = next_month_date; } pub fn set_prev_month(&mut self) { self.show_prev_month(); - self.selected = self.visible.clone(); + self.selected = self.visible; } pub fn set_next_month(&mut self) { self.show_next_month(); - self.selected = self.visible.clone(); + self.selected = self.visible; } pub fn set_selected_visible(&mut self, selected: NaiveDate) { self.selected = selected; - self.visible = self.selected.clone(); + self.visible = self.selected; } } diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 2930aadb..65961114 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -469,7 +469,7 @@ where text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) - .on_submit(on_update(ColorPickerUpdate::AppliedColor)) + .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor)) .leading_icon( color_button( None, @@ -611,7 +611,7 @@ pub struct ColorPicker<'a, Message> { must_clear_cache: Rc, } -impl<'a, Message> Widget for ColorPicker<'a, Message> +impl Widget for ColorPicker<'_, Message> where Message: Clone + 'static, { @@ -874,7 +874,7 @@ impl State { } } -impl<'a, Message> ColorPicker<'a, Message> where Message: Clone + 'static {} +impl ColorPicker<'_, Message> where Message: Clone + 'static {} // TODO convert active color to hex or rgba fn color_to_string(c: palette::Hsv, is_hex: bool) -> String { let srgb = palette::Srgb::from_color(c); diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index cc62b2db..c4b779ac 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -17,8 +17,7 @@ pub(super) struct Overlay<'a, 'b, Message> { pub(super) width: f32, } -impl<'a, 'b, Message> overlay::Overlay - for Overlay<'a, 'b, Message> +impl overlay::Overlay for Overlay<'_, '_, Message> where Message: Clone, { diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index fafa8a19..c59ae407 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -155,7 +155,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { } } -impl<'a, Message: Clone> Widget for ContextDrawer<'a, Message> { +impl Widget for ContextDrawer<'_, Message> { fn children(&self) -> Vec { vec![Tree::new(&self.content), Tree::new(&self.drawer)] } diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 338775b9..10f3ef3e 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -46,9 +46,7 @@ pub struct ContextMenu<'a, Message> { context_menu: Option>>, } -impl<'a, Message: Clone> Widget - for ContextMenu<'a, Message> -{ +impl Widget for ContextMenu<'_, Message> { fn tag(&self) -> tree::Tag { tree::Tag::of::() } diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 0c7907e4..3c8f181e 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -15,6 +15,12 @@ pub struct Dialog<'a, Message> { tertiary_action: Option>, } +impl Default for Dialog<'_, Message> { + fn default() -> Self { + Self::new() + } +} + impl<'a, Message> Dialog<'a, Message> { pub fn new() -> Self { Self { diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 4fcca830..31fa0305 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -243,8 +243,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { } } -impl<'a, Message: 'static> Widget - for DndDestination<'a, Message> +impl Widget + for DndDestination<'_, Message> { fn children(&self) -> Vec { vec![Tree::new(&self.container)] diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index d57e099b..4936ca10 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -111,7 +111,7 @@ impl< clipboard, false, if let Some(window) = self.window.as_ref() { - Some(iced_core::clipboard::DndSource::Surface(window.clone())) + Some(iced_core::clipboard::DndSource::Surface(*window)) } else { Some(iced_core::clipboard::DndSource::Widget(self.id.clone())) }, @@ -153,10 +153,9 @@ impl< } impl< - 'a, Message: Clone + 'static, D: iced::clipboard::mime::AsMimeTypes + std::marker::Send + 'static, - > Widget for DndSource<'a, Message, D> + > Widget for DndSource<'_, Message, D> { fn children(&self) -> Vec { vec![Tree::new(&self.container)] diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 045a5ef0..681a4c37 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -3,9 +3,13 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT mod appearance; +use std::borrow::Cow; +use std::sync::{Arc, Mutex}; + pub use appearance::{Appearance, StyleSheet}; -use crate::widget::{icon, Container}; +use crate::surface; +use crate::widget::{icon, Container, RcWrapper}; use iced_core::event::{self, Event}; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; @@ -21,13 +25,15 @@ use iced_widget::scrollable::Scrollable; pub struct Menu<'a, S, Message> where S: AsRef, + [S]: std::borrow::ToOwned, { - state: &'a mut State, - options: &'a [S], - icons: &'a [icon::Handle], - hovered_option: &'a mut Option, + state: State, + options: Cow<'a, [S]>, + icons: Cow<'a, [icon::Handle]>, + hovered_option: Arc>>, selected_option: Option, on_selected: Box Message + 'a>, + close_on_selected: Option, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, width: f32, padding: Padding, @@ -36,17 +42,21 @@ where style: (), } -impl<'a, S: AsRef, Message: 'a> Menu<'a, S, Message> { +impl<'a, S: AsRef, Message: 'a + std::clone::Clone> Menu<'a, S, Message> +where + [S]: std::borrow::ToOwned, +{ /// Creates a new [`Menu`] with the given [`State`], a list of options, and /// the message to produced when an option is selected. pub fn new( - state: &'a mut State, - options: &'a [S], - icons: &'a [icon::Handle], - hovered_option: &'a mut Option, + state: State, + options: Cow<'a, [S]>, + icons: Cow<'a, [icon::Handle]>, + hovered_option: Arc>>, selected_option: Option, on_selected: impl FnMut(usize) -> Message + 'a, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, + close_on_selected: Option, ) -> Self { Menu { state, @@ -61,6 +71,7 @@ impl<'a, S: AsRef, Message: 'a> Menu<'a, S, Message> { text_size: None, text_line_height: text::LineHeight::default(), style: Default::default(), + close_on_selected, } } @@ -102,20 +113,31 @@ impl<'a, S: AsRef, Message: 'a> Menu<'a, S, Message> { ) -> overlay::Element<'a, Message, crate::Theme, crate::Renderer> { overlay::Element::new(Box::new(Overlay::new(self, target_height, position))) } + + /// Turns the [`Menu`] into a popup [`Element`] at the given target + /// position. + /// + /// The `target_height` will be used to display the menu either on top + /// of the target or under it, depending on the screen position and the + /// dimensions of the [`Menu`]. + #[must_use] + pub fn popup(self, position: Point, target_height: f32) -> crate::Element<'a, Message> { + Overlay::new(self, target_height, position).into() + } } /// The local state of a [`Menu`]. #[must_use] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State { - tree: Tree, + pub(crate) tree: RcWrapper, } impl State { /// Creates a new [`State`] for a [`Menu`]. pub fn new() -> Self { Self { - tree: Tree::empty(), + tree: RcWrapper::new(Tree::empty()), } } } @@ -127,7 +149,7 @@ impl Default for State { } struct Overlay<'a, Message> { - state: &'a mut Tree, + state: RcWrapper, container: Container<'a, Message, crate::Theme, crate::Renderer>, width: f32, target_height: f32, @@ -135,12 +157,15 @@ struct Overlay<'a, Message> { position: Point, } -impl<'a, Message: 'a> Overlay<'a, Message> { +impl<'a, Message: Clone + 'a> Overlay<'a, Message> { pub fn new>( menu: Menu<'a, S, Message>, target_height: f32, position: Point, - ) -> Self { + ) -> Self + where + [S]: ToOwned, + { let Menu { state, options, @@ -154,6 +179,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { text_size, text_line_height, style, + close_on_selected, } = menu; let mut container = Container::new(Scrollable::new( @@ -163,6 +189,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { hovered_option, selected_option, on_selected, + close_on_selected, on_option_hovered, text_size, text_line_height, @@ -172,10 +199,12 @@ impl<'a, Message: 'a> Overlay<'a, Message> { )) .class(crate::style::Container::Dropdown); - state.tree.diff(&mut container as &mut dyn Widget<_, _, _>); + state + .tree + .with_data_mut(|tree| tree.diff(&mut container as &mut dyn Widget<_, _, _>)); Self { - state: &mut state.tree, + state: state.tree.clone(), container, width, target_height, @@ -183,20 +212,15 @@ impl<'a, Message: 'a> Overlay<'a, Message> { position, } } -} -impl<'a, Message> iced_core::Overlay - for Overlay<'a, Message> -{ - 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; + fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + let space_below = bounds.height - (self.position.y + self.target_height); + let space_above = self.position.y; let limits = layout::Limits::new( Size::ZERO, Size::new( - bounds.width - position.x, + bounds.width - self.position.x, if space_below > space_above { space_below } else { @@ -206,16 +230,18 @@ impl<'a, Message> iced_core::Overlay ) .width(self.width); - let node = self.container.layout(self.state, renderer, &limits); + let node = self + .state + .with_data_mut(|tree| self.container.layout(tree, renderer, &limits)); node.clone().move_to(if space_below > space_above { - position + Vector::new(0.0, self.target_height) + self.position + Vector::new(0.0, self.target_height) } else { - position - Vector::new(0.0, node.size().height) + self.position - Vector::new(0.0, node.size().height) }) } - fn on_event( + fn _on_event( &mut self, event: Event, layout: Layout<'_>, @@ -226,23 +252,27 @@ impl<'a, Message> iced_core::Overlay ) -> event::Status { let bounds = layout.bounds(); - self.container.on_event( - self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, - ) + self.state.with_data_mut(|tree| { + self.container.on_event( + tree, event, layout, cursor, renderer, clipboard, shell, &bounds, + ) + }) } - fn mouse_interaction( + fn _mouse_interaction( &self, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { - self.container - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + self.state.with_data(|tree| { + self.container + .mouse_interaction(tree, layout, cursor, viewport, renderer) + }) } - fn draw( + fn _draw( &self, renderer: &mut crate::Renderer, theme: &crate::Theme, @@ -266,25 +296,138 @@ impl<'a, Message> iced_core::Overlay appearance.background, ); - self.container - .draw(self.state, renderer, theme, style, layout, cursor, &bounds); + self.state.with_data(|tree| { + self.container + .draw(tree, renderer, theme, style, layout, cursor, &bounds) + }) } } -struct List<'a, S: AsRef, Message> { - options: &'a [S], - icons: &'a [icon::Handle], - hovered_option: &'a mut Option, +impl<'a, Message: Clone + 'a> iced_core::Overlay + for Overlay<'a, Message> +{ + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + self._layout(renderer, bounds) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self._on_event(event, layout, cursor, renderer, clipboard, shell) + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self._mouse_interaction(layout, cursor, viewport, renderer) + } + + fn draw( + &self, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self._draw(renderer, theme, style, layout, cursor); + } +} + +impl<'a, Message: Clone + 'a> crate::widget::Widget + for Overlay<'a, Message> +{ + fn size(&self) -> Size { + Size::new(Length::Fixed(self.width), Length::Shrink) + } + + fn layout( + &self, + _tree: &mut iced_core::widget::Tree, + renderer: &crate::Renderer, + limits: &iced::Limits, + ) -> layout::Node { + let limits = limits.width(self.width); + + self.state + .with_data_mut(|tree| self.container.layout(tree, renderer, &limits)) + } + + fn mouse_interaction( + &self, + _tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self._mouse_interaction(layout, cursor, viewport, renderer) + } + + fn on_event( + &mut self, + _tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) -> event::Status { + self._on_event(event, layout, cursor, renderer, clipboard, shell) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + self._draw(renderer, theme, style, layout, cursor); + } +} + +impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { + fn from(widget: Overlay<'a, Message>) -> Self { + Element::new(widget) + } +} + +struct List<'a, S: AsRef, Message> +where + [S]: std::borrow::ToOwned, +{ + options: Cow<'a, [S]>, + icons: Cow<'a, [icon::Handle]>, + hovered_option: Arc>>, selected_option: Option, on_selected: Box Message + 'a>, + close_on_selected: Option, on_option_hovered: Option<&'a dyn Fn(usize) -> Message>, padding: Padding, text_size: Option, text_line_height: text::LineHeight, } -impl<'a, S: AsRef, Message> Widget - for List<'a, S, Message> +impl, Message> Widget for List<'_, S, Message> +where + [S]: std::borrow::ToOwned, + Message: Clone, { fn size(&self) -> Size { Size::new(Length::Fill, Length::Shrink) @@ -330,9 +473,13 @@ impl<'a, S: AsRef, Message> Widget ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let hovered_guard = self.hovered_option.lock().unwrap(); if cursor.is_over(layout.bounds()) { - if let Some(index) = *self.hovered_option { + if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); + if let Some(close_on_selected) = self.close_on_selected.clone() { + shell.publish(close_on_selected); + } return event::Status::Captured; } } @@ -348,14 +495,15 @@ impl<'a, S: AsRef, Message> Widget + self.padding.vertical(); let new_hovered_option = (cursor_position.y / option_height) as usize; + let mut hovered_guard = self.hovered_option.lock().unwrap(); if let Some(on_option_hovered) = self.on_option_hovered { - if *self.hovered_option != Some(new_hovered_option) { + if *hovered_guard != Some(new_hovered_option) { shell.publish(on_option_hovered(new_hovered_option)); } } - *self.hovered_option = Some(new_hovered_option); + *hovered_guard = Some(new_hovered_option); } } Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -367,11 +515,15 @@ impl<'a, S: AsRef, Message> Widget let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.vertical(); + let mut hovered_guard = self.hovered_option.lock().unwrap(); - *self.hovered_option = Some((cursor_position.y / option_height) as usize); + *hovered_guard = Some((cursor_position.y / option_height) as usize); - if let Some(index) = *self.hovered_option { + if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); + if let Some(close_on_selected) = self.close_on_selected.clone() { + shell.publish(close_on_selected); + } return event::Status::Captured; } } @@ -434,6 +586,8 @@ impl<'a, S: AsRef, Message> Widget height: option_height, }; + let hovered_guard = self.hovered_option.lock().unwrap(); + let (color, font) = if self.selected_option == Some(i) { let item_x = bounds.x + appearance.border_width; let item_width = appearance.border_width.mul_add(-2.0, bounds.width); @@ -471,7 +625,7 @@ impl<'a, S: AsRef, Message> Widget ); (appearance.selected_text_color, crate::font::semibold()) - } else if *self.hovered_option == Some(i) { + } else if *hovered_guard == Some(i) { let item_x = bounds.x + appearance.border_width; let item_width = appearance.border_width.mul_add(-2.0, bounds.width); @@ -538,6 +692,9 @@ impl<'a, S: AsRef, Message> Widget impl<'a, S: AsRef, Message: 'a> From> for Element<'a, Message, crate::Theme, crate::Renderer> +where + [S]: std::borrow::ToOwned, + Message: Clone, { fn from(list: List<'a, S, Message>) -> Self { Element::new(list) diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index 6e3db648..3ebaffaf 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -5,6 +5,7 @@ //! Displays a list of options in a popover menu on select. pub mod menu; +use iced_core::window; pub use menu::Menu; pub mod multi; @@ -12,11 +13,40 @@ pub mod multi; mod widget; pub use widget::*; +use crate::surface; + /// Displays a list of options in a popover menu on select. -pub fn dropdown<'a, S: AsRef, Message: 'a>( - selections: &'a [S], +pub fn dropdown< + S: AsRef + std::clone::Clone + Send + Sync + 'static, + Message: 'static + Clone, +>( + selections: &[S], selected: Option, - on_selected: impl Fn(usize) -> Message + 'a, -) -> Dropdown<'a, S, Message> { + on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, +) -> Dropdown<'_, S, Message, Message> { Dropdown::new(selections, selected, on_selected) } + +/// Displays a list of options in a popover menu on select. +/// AppMessage must be the App's toplevel message. +pub fn popup_dropdown< + 'a, + S: AsRef + std::clone::Clone + Send + Sync + 'static, + Message: 'static + Clone, + AppMessage: 'static + Clone, +>( + selections: &'a [S], + selected: Option, + on_selected: impl Fn(usize) -> Message + Send + Sync + 'static, + _parent_id: window::Id, + _on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, + _map_action: impl Fn(Message) -> AppMessage + Send + Sync + 'static, +) -> Dropdown<'a, S, Message, AppMessage> { + let dropdown: Dropdown<'_, S, Message, AppMessage> = + Dropdown::new(selections, selected, on_selected); + + #[cfg(all(feature = "winit", feature = "wayland"))] + let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); + + dropdown +} diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index f5ee5f5b..3d37d928 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -180,9 +180,7 @@ impl<'a, Message: 'a> Overlay<'a, Message> { } } -impl<'a, Message> iced_core::Overlay - for Overlay<'a, Message> -{ +impl iced_core::Overlay for Overlay<'_, Message> { 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); @@ -279,8 +277,8 @@ struct InnerList<'a, S, Item, Message> { text_line_height: text::LineHeight, } -impl<'a, S, Item, Message> Widget - for InnerList<'a, S, Item, Message> +impl Widget + for InnerList<'_, S, Item, Message> where S: AsRef, Item: Clone + PartialEq, diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index b3bb09e2..14f89d72 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -159,7 +159,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> cursor: mouse::Cursor, viewport: &Rectangle, ) { - let font = self.font.unwrap_or_else(|| crate::font::default()); + let font = self.font.unwrap_or_else(crate::font::default); draw( renderer, @@ -278,7 +278,7 @@ pub fn layout( bounds: Size::new(f32::MAX, f32::MAX), size: iced::Pixels(text_size), line_height: text_line_height, - font: font.unwrap_or_else(|| crate::font::default()), + font: font.unwrap_or_else(crate::font::default), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, @@ -422,7 +422,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static bounds: Size::new(f32::MAX, f32::MAX), size: iced::Pixels(text_size), line_height, - font: font.unwrap_or_else(|| crate::font::default()), + font: font.unwrap_or_else(crate::font::default), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 26f69cce..f3388976 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -3,9 +3,10 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT use super::menu::{self, Menu}; -use crate::widget::icon; +use crate::widget::icon::{self, Handle}; +use crate::{surface, Element}; use derive_setters::Setters; -use iced::Radians; +use iced::window; use iced_core::event::{self, Event}; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; @@ -14,14 +15,24 @@ use iced_core::{ Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; use iced_widget::pick_list::{self, Catalog}; +use std::borrow::Cow; use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, LazyLock, Mutex}; +pub type DropdownView = Arc Element<'static, Message> + Send + Sync>; +static AUTOSIZE_ID: LazyLock = + LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize")); /// A widget for selecting a single value from a list of selections. #[derive(Setters)] -pub struct Dropdown<'a, S: AsRef, Message> { +pub struct Dropdown<'a, S: AsRef + Send + Sync + Clone + 'static, Message, AppMessage> +where + [S]: std::borrow::ToOwned, +{ #[setters(skip)] - on_selected: Box Message + 'a>, + on_selected: Arc Message + Send + Sync>, #[setters(skip)] selections: &'a [S], #[setters] @@ -38,9 +49,21 @@ pub struct Dropdown<'a, S: AsRef, Message> { text_line_height: text::LineHeight, #[setters(strip_option)] font: Option, + #[setters(skip)] + on_surface_action: Option Message + Send + Sync + 'static>>, + #[setters(skip)] + action_map: Option AppMessage + 'static + Send + Sync>>, + #[setters(strip_option)] + window_id: Option, + #[cfg(all(feature = "winit", feature = "wayland"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } -impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { +impl<'a, S: AsRef + Send + Sync + Clone + 'static, Message: 'static, AppMessage: 'static> + Dropdown<'a, S, Message, AppMessage> +where + [S]: std::borrow::ToOwned, +{ /// The default gap. pub const DEFAULT_GAP: f32 = 4.0; @@ -52,10 +75,10 @@ impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { pub fn new( selections: &'a [S], selected: Option, - on_selected: impl Fn(usize) -> Message + 'a, + on_selected: impl Fn(usize) -> Message + 'static + Send + Sync, ) -> Self { Self { - on_selected: Box::new(on_selected), + on_selected: Arc::new(on_selected), selections, icons: &[], selected, @@ -65,12 +88,73 @@ impl<'a, S: AsRef, Message> Dropdown<'a, S, Message> { text_size: None, text_line_height: text::LineHeight::Relative(1.2), font: None, + window_id: None, + #[cfg(all(feature = "winit", feature = "wayland"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), + on_surface_action: None, + action_map: None, } } + + #[cfg(all(feature = "winit", feature = "wayland"))] + /// Handle dropdown requests for popup creation. + /// Intended to be used with [`crate::app::message::get_popup`] + pub fn with_popup( + mut self, + parent_id: window::Id, + on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, + action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static, + ) -> Dropdown<'a, S, Message, NewAppMessage> { + let Self { + on_selected, + selections, + icons, + selected, + width, + gap, + padding, + text_size, + text_line_height, + font, + positioner, + .. + } = self; + + Dropdown::<'a, S, Message, NewAppMessage> { + on_selected, + selections, + icons, + selected, + width, + gap, + padding, + text_size, + text_line_height, + font, + on_surface_action: Some(Arc::new(on_surface_action)), + action_map: Some(Arc::new(action_map)), + window_id: Some(parent_id), + positioner, + } + } + + #[cfg(all(feature = "winit", feature = "wayland"))] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.positioner = positioner; + self + } } -impl<'a, S: AsRef, Message: 'a> Widget - for Dropdown<'a, S, Message> +impl< + S: AsRef + Send + Sync + Clone + 'static, + Message: 'static + Clone, + AppMessage: 'static + Clone, + > Widget for Dropdown<'_, S, Message, AppMessage> +where + [S]: std::borrow::ToOwned, { fn tag(&self) -> tree::Tag { tree::Tag::of::() @@ -153,15 +237,26 @@ impl<'a, S: AsRef, Message: 'a> Widget, _viewport: &Rectangle, ) -> event::Status { - update( + update::( &event, layout, cursor, shell, - self.on_selected.as_ref(), + #[cfg(all(feature = "winit", feature = "wayland"))] + self.positioner.clone(), + self.on_selected.clone(), self.selected, self.selections, || tree.state.downcast_mut::(), + self.window_id, + self.on_surface_action.clone(), + self.action_map.clone(), + self.icons, + self.gap, + self.padding, + self.text_size, + self.font, + self.selected, ) } @@ -186,7 +281,7 @@ impl<'a, S: AsRef, Message: 'a> Widget, Message: 'a> Widget Option> { + #[cfg(all(feature = "winit", feature = "wayland"))] + if self.window_id.is_some() || self.on_surface_action.is_some() { + return None; + } + let state = tree.state.downcast_mut::(); overlay( @@ -225,8 +325,9 @@ impl<'a, S: AsRef, Message: 'a> Widget, Message: 'a> Widget, Message: 'a> From> - for crate::Element<'a, Message> +impl< + 'a, + S: AsRef + Send + Sync + Clone + 'static, + Message: 'static + std::clone::Clone, + AppMessage: 'static + std::clone::Clone, + > From> for crate::Element<'a, Message> +where + [S]: std::borrow::ToOwned, { - fn from(pick_list: Dropdown<'a, S, Message>) -> Self { + fn from(pick_list: Dropdown<'a, S, Message, AppMessage>) -> Self { Self::new(pick_list) } } /// The local state of a [`Dropdown`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct State { icon: Option, menu: menu::State, keyboard_modifiers: keyboard::Modifiers, - is_open: bool, - hovered_option: Option, + is_open: Arc, + hovered_option: Arc>>, hashes: Vec, selections: Vec, + popup_id: window::Id, } impl State { @@ -276,10 +384,11 @@ impl State { }, menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), - is_open: false, - hovered_option: None, + is_open: Arc::new(AtomicBool::new(false)), + hovered_option: Arc::new(Mutex::new(None)), selections: Vec::new(), hashes: Vec::new(), + popup_id: window::Id::unique(), } } } @@ -316,7 +425,7 @@ pub fn layout( bounds: Size::new(f32::MAX, f32::MAX), size: iced::Pixels(text_size), line_height: text_line_height, - font: font.unwrap_or_else(|| crate::font::default()), + font: font.unwrap_or_else(crate::font::default), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, @@ -348,32 +457,136 @@ pub fn layout( /// Processes an [`Event`] and updates the [`State`] of a [`Dropdown`] /// accordingly. -#[allow(clippy::too_many_arguments)] -pub fn update<'a, S: AsRef, Message>( +#[allow(clippy::too_many_arguments, clippy::too_many_lines)] +pub fn update< + 'a, + S: AsRef + Send + Sync + Clone + 'static, + Message: Clone + 'static, + AppMessage: Clone + 'static, +>( event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - on_selected: &dyn Fn(usize) -> Message, + #[cfg(all(feature = "winit", feature = "wayland"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + on_selected: Arc Message + Send + Sync + 'static>, selected: Option, selections: &[S], state: impl FnOnce() -> &'a mut State, + _window_id: Option, + on_surface_action: Option Message + Send + Sync + 'static>>, + action_map: Option AppMessage + Send + Sync + 'static>>, + icons: &[icon::Handle], + gap: f32, + padding: Padding, + text_size: Option, + font: Option, + selected_option: Option, ) -> event::Status { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - - if state.is_open { + let is_open = state.is_open.load(Ordering::Relaxed); + if is_open { // Event wasn't processed by overlay, so cursor was clicked either outside it's // bounds or on the drop-down, either way we close the overlay. - state.is_open = false; - + state.is_open.store(false, Ordering::Relaxed); + #[cfg(all(feature = "winit", feature = "wayland"))] + if let Some(on_close) = on_surface_action { + shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); + } event::Status::Captured } else if cursor.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = selected; + state.is_open.store(true, Ordering::Relaxed); + let mut hovered_guard = state.hovered_option.lock().unwrap(); + *hovered_guard = selected; + let id = window::Id::unique(); + state.popup_id = id; + #[cfg(all(feature = "winit", feature = "wayland"))] + if let Some(((on_surface_action, parent), action_map)) = + on_surface_action.zip(_window_id).zip(action_map) + { + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + let bounds = layout.bounds(); + let anchor_rect = Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + }; + let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; + let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { + selection_paragraph.min_width().round() + }; + let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let selections_width = selections + .iter() + .zip(state.selections.iter_mut()) + .map(|(label, selection)| measure(label.as_ref(), selection.raw())) + .fold(0.0, |next, current| current.max(next)); + + let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec()); + let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec()); + let state = state.clone(); + let on_close = surface::action::destroy_popup(id); + let on_surface_action_clone = on_surface_action.clone(); + let get_popup_action = surface::action::simple_popup::< + AppMessage, + Box< + dyn Fn() -> Element<'static, crate::Action> + + Send + + Sync + + 'static, + >, + >( + move || { + SctkPopupSettings { + parent, + id, + input_zone: None, + positioner: SctkPositioner { + size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)), + anchor_rect, + // TODO: left or right alignment based on direction? + anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, + gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + reactive: true, + offset: (-padding.left as i32, 0), + constraint_adjustment: 9, + ..Default::default() + }, + parent_size: None, + grab: true, + close_with_children: true, + } + }, + Some(Box::new(move || { + let action_map = action_map.clone(); + let on_selected = on_selected.clone(); + let e: Element<'static, crate::Action> = + Element::from(menu_widget( + bounds, + &state, + gap, + padding, + text_size.unwrap_or(14.0), + selections.clone(), + icons.clone(), + selected_option, + Arc::new(move |i| on_selected.clone()(i)), + Some(on_surface_action_clone(on_close.clone())), + )) + .map(move |m| crate::Action::App(action_map.clone()(m))); + e + })), + ); + shell.publish(on_surface_action(get_popup_action)); + } event::Status::Captured } else { event::Status::Ignored @@ -383,11 +596,9 @@ pub fn update<'a, S: AsRef, Message>( delta: mouse::ScrollDelta::Lines { .. }, }) => { let state = state(); + let is_open = state.is_open.load(Ordering::Relaxed); - if state.keyboard_modifiers.command() - && cursor.is_over(layout.bounds()) - && !state.is_open - { + if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open { let next_index = selected.map(|index| index + 1).unwrap_or_default(); if selections.len() < next_index { @@ -423,9 +634,72 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } +#[cfg(all(feature = "winit", feature = "wayland"))] +/// Returns the current menu widget of a [`Dropdown`]. +#[allow(clippy::too_many_arguments)] +pub fn menu_widget< + S: AsRef + Send + Sync + Clone + 'static, + Message: 'static + std::clone::Clone, +>( + bounds: Rectangle, + state: &State, + gap: f32, + padding: Padding, + text_size: f32, + selections: Cow<'static, [S]>, + icons: Cow<'static, [icon::Handle]>, + selected_option: Option, + on_selected: Arc Message + Send + Sync + 'static>, + close_on_selected: Option, +) -> crate::Element<'static, Message> +where + [S]: std::borrow::ToOwned, +{ + let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; + let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { + selection_paragraph.min_width().round() + }; + let selections_width = selections + .iter() + .zip(state.selections.iter()) + .map(|(label, selection)| measure(label.as_ref(), selection.raw())) + .fold(0.0, |next, current| current.max(next)); + let pad_width = padding.horizontal().mul_add(2.0, 16.0); + + let width = selections_width + gap + pad_width + icon_width; + let is_open = state.is_open.clone(); + let menu: Menu<'static, S, Message> = Menu::new( + state.menu.clone(), + selections, + icons, + state.hovered_option.clone(), + selected_option, + move |option| { + is_open.store(false, Ordering::Relaxed); + + (on_selected)(option) + }, + None, + close_on_selected, + ) + .width(width) + .padding(padding) + .text_size(text_size); + + crate::widget::autosize::autosize( + menu.popup(iced::Point::new(0., 0.), bounds.height), + AUTOSIZE_ID.clone(), + ) + .auto_height(true) + .auto_width(true) + .min_height(1.) + .min_width(width) + .into() +} + /// Returns the current overlay of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] -pub fn overlay<'a, S: AsRef, Message: 'a>( +pub fn overlay<'a, S: AsRef + Send + Sync + Clone + 'static, Message: std::clone::Clone + 'a>( layout: Layout<'_>, _renderer: &crate::Renderer, state: &'a mut State, @@ -439,22 +713,27 @@ pub fn overlay<'a, S: AsRef, Message: 'a>( selected_option: Option, on_selected: &'a dyn Fn(usize) -> Message, translation: Vector, -) -> Option> { - if state.is_open { + close_on_selected: Option, +) -> Option> +where + [S]: std::borrow::ToOwned, +{ + if state.is_open.load(Ordering::Relaxed) { let bounds = layout.bounds(); let menu = Menu::new( - &mut state.menu, - selections, - icons, - &mut state.hovered_option, + state.menu.clone(), + Cow::Borrowed(selections), + Cow::Borrowed(icons), + state.hovered_option.clone(), selected_option, |option| { - state.is_open = false; + state.is_open.store(false, Ordering::Relaxed); (on_selected)(option) }, None, + close_on_selected, ) .width({ let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 49955217..852c18cc 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -85,9 +85,7 @@ impl<'a, Message> FlexRow<'a, Message> { } } -impl<'a, Message: 'static + Clone> Widget - for FlexRow<'a, Message> -{ +impl Widget for FlexRow<'_, Message> { fn children(&self) -> Vec { self.children.iter().map(Tree::new).collect() } diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 961ffb90..fc91ba29 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -44,6 +44,12 @@ pub struct Grid<'a, Message> { row: u16, } +impl Default for Grid<'_, Message> { + fn default() -> Self { + Self::new() + } +} + impl<'a, Message> Grid<'a, Message> { pub const fn new() -> Self { Self { @@ -106,7 +112,7 @@ impl<'a, Message> Grid<'a, Message> { } } -impl<'a, Message: 'static + Clone> Widget for Grid<'a, Message> { +impl Widget for Grid<'_, Message> { fn children(&self) -> Vec { self.children.iter().map(Tree::new).collect() } @@ -303,6 +309,12 @@ pub struct Assignment { pub(super) height: u16, } +impl Default for Assignment { + fn default() -> Self { + Self::new() + } +} + impl Assignment { pub const fn new() -> Self { Self { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1f70220f..32bfa278 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -120,8 +120,8 @@ pub struct HeaderBarWidget<'a, Message> { header_bar_inner: Element<'a, Message>, } -impl<'a, Message: Clone + 'static> Widget - for HeaderBarWidget<'a, Message> +impl Widget + for HeaderBarWidget<'_, Message> { fn diff(&mut self, tree: &mut tree::Tree) { tree.diff_children(&mut [&mut self.header_bar_inner]); @@ -306,7 +306,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { Density::Spacious => 48.0, Density::Standard => 48.0, }; - + let portion = ((start.len().max(end.len()) as f32 / center.len().max(1) as f32).round() + as u16) + .max(1); + let center_empty = center.is_empty() && self.title.is_empty(); // Creates the headerbar widget. let mut widget = widget::row::with_capacity(3) // If elements exist in the start region, append them here. @@ -316,7 +319,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::Alignment::Start) - .width(Length::Shrink), + .width(Length::FillPortion(portion)), ) // If elements exist in the center region, use them here. // This will otherwise use the title as a widget if a title was defined. @@ -338,7 +341,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::Alignment::End) - .width(Length::Shrink), + .width(if center_empty { + Length::Fill + } else { + Length::FillPortion(portion) + }), ) .align_y(iced::Alignment::Center) .height(Length::Fixed(height)) diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 318b9fb1..45d5451d 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -84,7 +84,7 @@ impl Icon { self.height .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), ) - .rotation(self.rotation.unwrap_or_else(Rotation::default)) + .rotation(self.rotation.unwrap_or_default()) .content_fit(self.content_fit) .into() }; @@ -100,7 +100,7 @@ impl Icon { self.height .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), ) - .rotation(self.rotation.unwrap_or_else(Rotation::default)) + .rotation(self.rotation.unwrap_or_default()) .content_fit(self.content_fit) .symbolic(self.handle.symbolic) .into() diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 40432c04..055b2e42 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -144,7 +144,7 @@ impl From for Icon { } } -impl<'a, Message: 'static> From for crate::Element<'a, Message> { +impl From for crate::Element<'_, Message> { fn from(builder: Named) -> Self { builder.icon().into() } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index ae30289f..2070780b 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -47,8 +47,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget - for IdContainer<'a, Message, Theme, Renderer> +impl Widget + for IdContainer<'_, Message, Theme, Renderer> where Renderer: iced_core::Renderer, { diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 016932b4..f442bc51 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -138,8 +138,7 @@ where } } -impl<'a, Message, Renderer> Widget - for LayerContainer<'a, Message, Renderer> +impl Widget for LayerContainer<'_, Message, Renderer> where Renderer: iced_core::Renderer, { diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 39eb96a1..1a9b7348 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -24,7 +24,7 @@ pub struct ListColumn<'a, Message> { children: Vec>, } -impl<'a, Message: 'static> Default for ListColumn<'a, Message> { +impl Default for ListColumn<'_, Message> { fn default() -> Self { let cosmic_theme::Spacing { space_xxs, space_m, .. diff --git a/src/widget/menu.rs b/src/widget/menu.rs index d5ea3222..f5c9c461 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -55,6 +55,7 @@ //! pub mod action; + pub use action::MenuAction as Action; mod flex; diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index a6934ff1..8b4ed227 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -40,7 +40,7 @@ impl KeyBind { pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool { let key_eq = match (key, &self.key) { // CapsLock and Shift change the case of Key::Character, so we compare these in a case insensitive way - (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(&b), + (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b), (a, b) => a.eq(b), }; key_eq diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 1f112925..283e5922 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -64,8 +64,8 @@ impl Default for MenuBarState { } } -pub(crate) fn menu_roots_children<'a, Message, Renderer>( - menu_roots: &Vec>, +pub(crate) fn menu_roots_children( + menu_roots: &Vec>, ) -> Vec where Renderer: renderer::Renderer, @@ -95,8 +95,8 @@ where } #[allow(invalid_reference_casting)] -pub(crate) fn menu_roots_diff<'a, Message, Renderer>( - menu_roots: &mut Vec>, +pub(crate) fn menu_roots_diff( + menu_roots: &mut Vec>, tree: &mut Tree, ) where Renderer: renderer::Renderer, @@ -280,8 +280,7 @@ where self } } -impl<'a, Message, Renderer> Widget - for MenuBar<'a, Message, Renderer> +impl Widget for MenuBar<'_, Message, Renderer> where Renderer: renderer::Renderer, { @@ -366,6 +365,8 @@ where if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { state.view_cursor = view_cursor; state.open = true; + // #[cfg(feature = "wayland")] + // TODO emit Message to open menu } } _ => (), @@ -437,6 +438,9 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { + // #[cfg(feature = "wayland")] + // return None; + let state = tree.state.downcast_ref::(); if !state.open { return None; diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index b64fba2c..1cd60dec 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -447,7 +447,7 @@ where pub(crate) style: &'b ::Style, pub(crate) position: Point, } -impl<'a, 'b, Message, Renderer> Menu<'a, 'b, Message, Renderer> +impl<'b, Message, Renderer> Menu<'_, 'b, Message, Renderer> where Renderer: renderer::Renderer, { @@ -455,8 +455,8 @@ where overlay::Element::new(Box::new(self)) } } -impl<'a, 'b, Message, Renderer> overlay::Overlay - for Menu<'a, 'b, Message, Renderer> +impl overlay::Overlay + for Menu<'_, '_, Message, Renderer> where Renderer: renderer::Renderer, { diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 01ca3076..86ed0dfb 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -9,9 +9,9 @@ use std::rc::Rc; use iced_widget::core::{renderer, Element}; use crate::iced_core::{Alignment, Length}; -use crate::widget::icon; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; +use crate::widget::{icon, Button}; use crate::{theme, widget}; /// Nested menu is essentially a tree of items, a menu is a collection of items @@ -192,14 +192,13 @@ pub enum MenuItem>> { /// - A button for the root menu item. pub fn menu_root<'a, Message, Renderer: renderer::Renderer>( label: impl Into> + 'a, -) -> iced::Element<'a, Message, crate::Theme, Renderer> +) -> Button<'a, Message> where Element<'a, Message, crate::Theme, Renderer>: From>, { widget::button::custom(widget::text(label)) .padding([4, 12]) .class(theme::Button::MenuRoot) - .into() } /// Create a list of menu items from a vector of `MenuItem`. diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 06dd4e85..75a3191c 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -97,6 +97,14 @@ pub mod aspect_ratio; #[cfg(feature = "autosize")] pub mod autosize; +pub(crate) mod responsive_container; + +#[cfg(feature = "surface-message")] +mod responsive_menu_bar; +#[cfg(feature = "surface-message")] +#[doc(inline)] +pub use responsive_menu_bar::responsive_menu_bar; + pub mod button; #[doc(inline)] pub use button::{Button, IconButton, LinkButton, TextButton}; @@ -335,9 +343,12 @@ pub use toggler::toggler; #[doc(inline)] pub use tooltip::{tooltip, Tooltip}; + +#[cfg(all(feature = "wayland", feature = "winit"))] +pub mod wayland; + pub mod tooltip { use crate::Element; - use std::borrow::Cow; pub use iced::widget::tooltip::Position; @@ -362,6 +373,10 @@ pub mod warning; #[doc(inline)] pub use warning::*; +pub mod wrapper; +#[doc(inline)] +pub use wrapper::*; + #[cfg(feature = "markdown")] #[doc(inline)] pub use iced::widget::markdown; diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 1d807060..2a315683 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -25,7 +25,7 @@ pub fn nav_bar_toggle() -> NavBarToggle { } } -impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { +impl From> for Element<'_, Message> { fn from(nav_bar_toggle: NavBarToggle) -> Self { let icon = if nav_bar_toggle.active { widget::icon::from_svg_bytes( diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 974153d3..12a64d06 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -75,8 +75,8 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { // TODO More options for positioning similar to GdkPopup, xdg_popup } -impl<'a, Message: Clone, Renderer> Widget - for Popover<'a, Message, Renderer> +impl Widget + for Popover<'_, Message, Renderer> where Renderer: iced_core::Renderer, { @@ -305,8 +305,8 @@ pub struct Overlay<'a, 'b, Message, Renderer> { pos: Point, } -impl<'a, 'b, Message, Renderer> overlay::Overlay - for Overlay<'a, 'b, Message, Renderer> +impl overlay::Overlay + for Overlay<'_, '_, Message, Renderer> where Message: Clone, Renderer: iced_core::Renderer, @@ -425,7 +425,7 @@ where ) -> Option> { self.content .as_widget_mut() - .overlay(&mut self.tree, layout, renderer, Default::default()) + .overlay(self.tree, layout, renderer, Default::default()) } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 55d96192..ebb75ee2 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -155,7 +155,7 @@ where } } -impl<'a, Message, Renderer> Widget for Radio<'a, Message, Renderer> +impl Widget for Radio<'_, Message, Renderer> where Message: Clone, Renderer: iced_core::Renderer, diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 876b1255..3e7753e9 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -209,7 +209,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let layout = self.container.layout( + self.container.layout( tree, renderer, if self.ignore_bounds { @@ -217,9 +217,7 @@ where } else { limits }, - ); - - layout + ) } fn operate( diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs new file mode 100644 index 00000000..82b91824 --- /dev/null +++ b/src/widget/responsive_container.rs @@ -0,0 +1,299 @@ +//! Responsive Container, which will notify of size changes. + +use iced::{Limits, Size}; +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, Id, Tree}; +use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; + +pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( + content: E, + id: Id, + on_action: impl Fn(crate::surface::Action) -> Message + 'static, +) -> ResponsiveContainer<'a, Message, Theme, crate::Renderer> +where + E: Into>, + Theme: iced_widget::container::Catalog, + ::Class<'a>: From>, +{ + ResponsiveContainer::new(content, id, on_action) +} + +/// An element decorating some content. +/// +/// It is normally used for alignment purposes. +#[allow(missing_debug_implementations)] +pub struct ResponsiveContainer<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + id: Id, + size: Option, + on_action: Box Message>, +} + +impl<'a, Message, Theme, Renderer> ResponsiveContainer<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + /// Creates an empty [`IdContainer`]. + pub(crate) fn new( + content: T, + id: Id, + on_action: impl Fn(crate::surface::Action) -> Message + 'static, + ) -> Self + where + T: Into>, + { + ResponsiveContainer { + content: content.into(), + id, + size: None, + on_action: Box::new(on_action), + } + } + + pub(crate) fn size(mut self, size: Size) -> Self { + self.size = Some(size); + self + } +} + +impl Widget + for ResponsiveContainer<'_, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::new()) + } + + 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 state = tree.state.downcast_mut::(); + let unrestricted_size = self.size.unwrap_or_else(|| { + let node = + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, &Limits::NONE); + node.size() + }); + + let max_size = limits.max(); + let old_max = state.limits.max(); + state.needs_update = (unrestricted_size.width > max_size.width) + ^ (state.size.width > old_max.width) + || (unrestricted_size.height > max_size.height) ^ (state.size.height > old_max.height); + if state.needs_update { + state.limits = *limits; + state.size = unrestricted_size; + } + + let node = self + .content + .as_widget() + .layout(&mut tree.children[0], renderer, 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 { + let state = tree.state.downcast_mut::(); + + if state.needs_update { + shell.publish((self.on_action)( + crate::surface::Action::ResponsiveMenuBar { + menu_bar: self.id.clone(), + limits: state.limits, + size: state.size, + }, + )); + state.needs_update = false; + } + + 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; + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + let c_state = &state.children[0]; + self.content.as_widget().a11y_nodes(c_layout, c_state, p) + } +} + +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: ResponsiveContainer<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(c) + } +} + +#[derive(Debug, Clone, Copy)] +struct State { + limits: Limits, + size: Size, + needs_update: bool, +} + +impl State { + fn new() -> Self { + Self { + limits: Limits::NONE, + size: Size::new(0., 0.), + needs_update: false, + } + } +} diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs new file mode 100644 index 00000000..38857100 --- /dev/null +++ b/src/widget/responsive_menu_bar.rs @@ -0,0 +1,78 @@ +use std::collections::HashMap; + +use apply::Apply; + +use crate::{ + widget::{button, icon, responsive_container}, + Core, Element, +}; + +use super::menu; + +/// # Panics +/// +/// Will panic if the menu bar collapses without tracking the size +pub fn responsive_menu_bar<'a, Message: Clone + 'static, A: menu::Action>( + core: &Core, + key_binds: &HashMap, + id: crate::widget::Id, + action_message: impl Fn(crate::surface::Action) -> Message + 'static, + trees: Vec<( + std::borrow::Cow<'static, str>, + Vec>>, + )>, +) -> Element<'a, Message> { + use crate::widget::id_container; + + let menu_bar_size = core.menu_bars.get(&id); + + #[allow(clippy::if_not_else)] + if !menu_bar_size.is_some_and(|(limits, size)| { + let max_size = limits.max(); + max_size.width < size.width + }) { + responsive_container::responsive_container( + id_container( + menu::bar( + trees + .into_iter() + .map(|mt| { + menu::Tree::<_>::with_children( + menu::root(mt.0), + menu::items(key_binds, mt.1), + ) + }) + .collect(), + ), + crate::widget::Id::new(format!("menu_bar_expanded_{id}")), + ), + id, + action_message, + ) + .apply(Element::from) + } else { + responsive_container::responsive_container( + id_container( + menu::bar(vec![menu::Tree::<_>::with_children( + Element::from( + button::icon(icon::from_name("open-menu-symbolic")) + .padding([4, 12]) + .class(crate::theme::Button::MenuRoot), + ), + menu::items( + key_binds, + trees + .into_iter() + .map(|mt| menu::Item::Folder(mt.0, mt.1)) + .collect(), + ), + )]), + crate::widget::Id::new(format!("menu_bar_collapsed_{id}")), + ), + id, + action_message, + ) + .size(menu_bar_size.unwrap().1) + .apply(Element::from) + } +} diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index a859b2ca..ccf4c8ae 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -30,8 +30,8 @@ where SegmentedButton::new(model) } -impl<'a, SelectionMode, Message> SegmentedVariant - for SegmentedButton<'a, Horizontal, SelectionMode, Message> +impl SegmentedVariant + for SegmentedButton<'_, Horizontal, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, diff --git a/src/widget/segmented_button/model/entity.rs b/src/widget/segmented_button/model/entity.rs index 77f591b9..02a7d371 100644 --- a/src/widget/segmented_button/model/entity.rs +++ b/src/widget/segmented_button/model/entity.rs @@ -15,7 +15,7 @@ pub struct EntityMut<'a, SelectionMode: Default> { pub(super) model: &'a mut Model, } -impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode> +impl EntityMut<'_, SelectionMode> where Model: Selectable, { diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index d8ae0be9..3f5d5645 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -30,8 +30,8 @@ where SegmentedButton::new(model) } -impl<'a, SelectionMode, Message> SegmentedVariant - for SegmentedButton<'a, Vertical, SelectionMode, Message> +impl SegmentedVariant + for SegmentedButton<'_, Vertical, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index e40465d0..0e433aec 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -547,8 +547,8 @@ where } } -impl<'a, Variant, SelectionMode, Message> Widget - for SegmentedButton<'a, Variant, SelectionMode, Message> +impl Widget + for SegmentedButton<'_, Variant, SelectionMode, Message> where Self: SegmentedVariant, Model: Selectable, @@ -562,7 +562,7 @@ where if let Some(ref context_menu) = self.context_menu { let mut tree = Tree::empty(); tree.state = tree::State::new(MenuBarState::default()); - tree.children = menu_roots_children(&context_menu); + tree.children = menu_roots_children(context_menu); children.push(tree); } @@ -719,7 +719,7 @@ where let on_dnd_enter = self.on_dnd_enter .as_ref() - .zip(entity.clone()) + .zip(entity) .map(|(on_enter, entity)| { move |_, _, mime_types| on_enter(entity, mime_types) }); diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index e32251ef..71b9a92e 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -20,9 +20,7 @@ pub fn section<'a, Message: 'static>() -> Section<'a, Message> { } /// A section with a pre-defined list column. -pub fn with_column<'a, Message: 'static>( - children: ListColumn<'a, Message>, -) -> Section<'a, Message> { +pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { Section { title: Cow::Borrowed(""), children, diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 10c19125..8739342d 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -9,12 +9,10 @@ use crate::{ Element, }; use apply::Apply; -use derive_setters::Setters; -use iced::{alignment::Horizontal, Border, Shadow}; use iced::{Alignment, Length}; -use std::marker::PhantomData; +use iced::{Border, Shadow}; +use std::borrow::Cow; use std::ops::{Add, Sub}; -use std::{borrow::Cow, fmt::Display}; /// Horizontal spin button widget. pub fn spin_button<'a, T, M>( @@ -153,9 +151,7 @@ where } } -fn horizontal_variant<'a, T, Message>( - spin_button: SpinButton<'a, T, Message>, -) -> Element<'a, Message> +fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, @@ -193,7 +189,7 @@ where .into() } -fn vertical_variant<'a, T, Message>(spin_button: SpinButton<'a, T, Message>) -> Element<'a, Message> +fn vertical_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 8d46b839..d6c11cf6 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -18,7 +18,6 @@ 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; @@ -40,10 +39,6 @@ use iced_core::{ Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, Vector, Widget, }; -#[cfg(feature = "wayland")] -use iced_renderer::core::event::{wayland, PlatformSpecific}; -#[cfg(feature = "wayland")] -use iced_runtime::platform_specific; use iced_runtime::{task, Action, Task}; thread_local! { @@ -200,7 +195,7 @@ pub struct TextInput<'a, Message> { error: Option>, on_input: Option Message + 'a>>, on_paste: Option Message + 'a>>, - on_submit: Option, + on_submit: Option Message + 'a>>, on_toggle_edit: Option Message + 'a>>, leading_icon: Option>, trailing_icon: Option>, @@ -211,6 +206,8 @@ pub struct TextInput<'a, Message> { line_height: text::LineHeight, helper_line_height: text::LineHeight, always_active: bool, + /// The text input tracks and manages the input value in its state. + manage_value: bool, } impl<'a, Message> TextInput<'a, Message> @@ -255,6 +252,7 @@ where label: None, helper_text: None, always_active: false, + manage_value: false, } } @@ -340,14 +338,24 @@ where /// Sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - pub fn on_submit(self, message: Message) -> Self { - self.on_submit_maybe(Some(message)) + pub fn on_submit(self, callback: F) -> Self + where + F: 'a + Fn(String) -> Message, + { + self.on_submit_maybe(Some(Box::new(callback))) } /// Maybe sets the message that should be produced when the [`TextInput`] is /// focused and the enter key is pressed. - pub fn on_submit_maybe(mut self, message: Option) -> Self { - self.on_submit = message; + pub fn on_submit_maybe(mut self, callback: Option) -> Self + where + F: 'a + Fn(String) -> Message, + { + if let Some(callback) = callback { + self.on_submit = Some(Box::new(callback)); + } else { + self.on_submit = None; + } self } @@ -416,6 +424,12 @@ where self } + /// Sets the text input to manage its input value or not + pub fn manage_value(mut self, manage_value: bool) -> Self { + self.manage_value = true; + self + } + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -507,7 +521,7 @@ where } } -impl<'a, Message> Widget for TextInput<'a, Message> +impl Widget for TextInput<'_, Message> where Message: Clone + 'static, { @@ -526,9 +540,14 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); - + if !self.manage_value || !self.value.is_empty() && state.tracked_value != self.value { + state.tracked_value = self.value.clone(); + } else if self.value.is_empty() { + self.value = state.tracked_value.clone(); + // std::mem::swap(&mut state.tracked_value, &mut self.value); + } // Unfocus text input if it becomes disabled - if self.on_input.is_none() { + if self.on_input.is_none() && !self.manage_value { state.last_click = None; state.is_focused = None; state.is_pasting = None; @@ -581,13 +600,10 @@ where // if the previous state was at the end of the text, keep it there let old_value = Value::new(&old_value); if state.is_focused.is_some() { - match state.cursor.state(&old_value) { - cursor::State::Index(index) => { - if index == old_value.len() { - state.cursor.move_to(self.value.len()); - } + if let cursor::State::Index(index) = state.cursor.state(&old_value) { + if index == old_value.len() { + state.cursor.move_to(self.value.len()); } - _ => {} }; } @@ -597,6 +613,11 @@ where state.is_focused = None; } + // Stop pasting if input becomes disabled + if !self.manage_value && self.on_input.is_none() { + state.is_pasting = None; + } + let mut children: Vec<_> = self .leading_icon .iter_mut() @@ -779,7 +800,7 @@ where } } - if tree.children.len() > 0 { + if !tree.children.is_empty() { let index = tree.children.len() - 1; if let (Some(trailing_icon), Some(tree)) = (self.trailing_icon.as_mut(), tree.children.get_mut(index)) @@ -824,13 +845,14 @@ where self.is_editable, self.on_input.as_deref(), self.on_paste.as_deref(), - &self.on_submit, + self.on_submit.as_deref(), self.on_toggle_edit.as_deref(), || tree.state.downcast_mut::(), self.on_create_dnd_source.as_deref(), dnd_id, line_height, layout, + self.manage_value, ) } @@ -856,7 +878,7 @@ where &self.placeholder, self.size, self.font, - self.on_input.is_none(), + self.on_input.is_none() && !self.manage_value, self.is_secure, self.leading_icon.as_ref(), self.trailing_icon.as_ref(), @@ -925,7 +947,11 @@ where } let mut children = layout.children(); let layout = children.next().unwrap(); - mouse_interaction(layout, cursor_position, self.on_input.is_none()) + mouse_interaction( + layout, + cursor_position, + self.on_input.is_none() && !self.manage_value, + ) } fn id(&self) -> Option { @@ -1236,13 +1262,14 @@ pub fn update<'a, Message: 'static>( is_editable: bool, on_input: Option<&dyn Fn(String) -> Message>, on_paste: Option<&dyn Fn(String) -> Message>, - on_submit: &Option, + on_submit: Option<&dyn Fn(String) -> 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_id: u128, line_height: text::LineHeight, layout: Layout<'_>, + manage_value: bool, ) -> event::Status where Message: Clone, @@ -1264,7 +1291,7 @@ where | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state(); - let click_position = if on_input.is_some() { + let click_position = if on_input.is_some() || manage_value { cursor.position_over(layout.bounds()) } else { None @@ -1299,7 +1326,7 @@ where // single click that is on top of the selected text // is the click on selected text? - if let Some(on_input) = on_input { + if manage_value || on_input.is_some() { let left = start.min(end); let right = end.max(start); @@ -1339,8 +1366,11 @@ where let contents = editor.contents(); let unsecured_value = Value::new(&contents); - let message = (on_input)(contents); - shell.publish(message); + state.tracked_value = unsecured_value.clone(); + if let Some(on_input) = on_input { + let message = (on_input)(contents); + shell.publish(message); + } if let Some(on_start_dnd) = on_start_dnd_source { shell.publish(on_start_dnd(state.clone())); } @@ -1349,7 +1379,7 @@ where iced_core::clipboard::start_dnd( clipboard, false, - id.map(|id| iced_core::clipboard::DndSource::Widget(id)), + id.map(iced_core::clipboard::DndSource::Widget), Some(iced_core::clipboard::IconSurface::new( Element::from( TextInput::<'static, ()>::new("", input_text.clone()) @@ -1531,7 +1561,7 @@ where let state = state(); if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { + if !manage_value && on_input.is_none() { return event::Status::Ignored; }; @@ -1545,8 +1575,8 @@ where match key { keyboard::Key::Named(keyboard::key::Named::Enter) => { - if let Some(on_submit) = on_submit.clone() { - shell.publish(on_submit); + if let Some(on_submit) = on_submit { + shell.publish((on_submit)(unsecured_value.to_string())); } } keyboard::Key::Named(keyboard::key::Named::Backspace) => { @@ -1566,9 +1596,11 @@ where let contents = editor.contents(); let unsecured_value = Value::new(&contents); - let message = (on_input)(editor.contents()); - shell.publish(message); - + state.tracked_value = unsecured_value.clone(); + if let Some(on_input) = on_input { + let message = (on_input)(editor.contents()); + shell.publish(message); + } let value = if is_secure { unsecured_value.secure() } else { @@ -1592,8 +1624,12 @@ where editor.delete(); let contents = editor.contents(); let unsecured_value = Value::new(&contents); - let message = (on_input)(contents); - shell.publish(message); + if let Some(on_input) = on_input { + let message = (on_input)(contents); + state.tracked_value = unsecured_value.clone(); + shell.publish(message); + } + let value = if is_secure { unsecured_value.secure() } else { @@ -1671,10 +1707,12 @@ where let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - - let message = (on_input)(editor.contents()); - - shell.publish(message); + let content = editor.contents(); + state.tracked_value = Value::new(&content); + if let Some(on_input) = on_input { + let message = (on_input)(content); + shell.publish(message); + } } } keyboard::Key::Character(c) @@ -1699,13 +1737,16 @@ where let contents = editor.contents(); let unsecured_value = Value::new(&contents); - let message = if let Some(paste) = &on_paste { - (paste)(contents) - } else { - (on_input)(contents) - }; - shell.publish(message); + state.tracked_value = unsecured_value.clone(); + if let Some(on_input) = on_input { + let message = if let Some(paste) = &on_paste { + (paste)(contents) + } else { + (on_input)(contents) + }; + shell.publish(message); + } state.is_pasting = Some(content); let value = if is_secure { @@ -1750,8 +1791,11 @@ where } let contents = editor.contents(); let unsecured_value = Value::new(&contents); - let message = (on_input)(contents); - shell.publish(message); + state.tracked_value = unsecured_value.clone(); + if let Some(on_input) = on_input { + let message = (on_input)(contents); + shell.publish(message); + } focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); @@ -1926,7 +1970,7 @@ where editor.paste(Value::new(content.as_str())); let contents = editor.contents(); let unsecured_value = Value::new(&contents); - + state.tracked_value = unsecured_value.clone(); if let Some(on_paste) = on_paste.as_ref() { let message = (on_paste)(contents); shell.publish(message); @@ -2408,6 +2452,7 @@ pub(crate) struct DndOfferState; #[derive(Debug, Default, Clone)] #[must_use] pub struct State { + pub tracked_value: Value, pub value: crate::Plain, pub placeholder: crate::Plain, pub label: crate::Plain, @@ -2482,6 +2527,7 @@ impl State { /// Creates a new [`State`], representing a focused [`TextInput`]. pub fn focused(is_secure: bool, is_read_only: bool) -> Self { Self { + tracked_value: Value::default(), is_secure, value: crate::Plain::default(), placeholder: crate::Plain::default(), diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index b18ea2ca..dee3f110 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -8,7 +8,7 @@ use unicode_segmentation::UnicodeSegmentation; /// /// [`TextInput`]: crate::widget::TextInput // TODO: Reduce allocations, cache results (?) -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct Value { graphemes: Vec, } diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index 2ed8289e..1acbce0c 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 iced::{Padding, Task}; +use iced::Task; use iced_core::Element; use slotmap::new_key_type; use slotmap::SlotMap; diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 7a7f6949..bde5c890 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -35,8 +35,8 @@ impl<'a, Message, Theme, Renderer> Toaster<'a, Message, Theme, Renderer> { } } -impl<'a, Message, Theme, Renderer> Widget - for Toaster<'a, Message, Theme, Renderer> +impl Widget + for Toaster<'_, Message, Theme, Renderer> where Renderer: iced_core::Renderer, { @@ -191,8 +191,8 @@ where } } -impl<'a, 'b, Message, Theme, Renderer> Overlay - for ToasterOverlay<'a, 'b, Message, Theme, Renderer> +impl Overlay + for ToasterOverlay<'_, '_, Message, Theme, Renderer> where Renderer: renderer::Renderer, { diff --git a/src/widget/wayland/mod.rs b/src/widget/wayland/mod.rs new file mode 100644 index 00000000..7c53d374 --- /dev/null +++ b/src/widget/wayland/mod.rs @@ -0,0 +1 @@ +pub mod tooltip; diff --git a/src/widget/wayland/tooltip/mod.rs b/src/widget/wayland/tooltip/mod.rs new file mode 100644 index 00000000..79a2fda9 --- /dev/null +++ b/src/widget/wayland/tooltip/mod.rs @@ -0,0 +1,76 @@ +//! Change the apperance of a tooltip. + +pub mod widget; + +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced_core::{border::Radius, Background, Color, Vector}; + +use crate::theme::THEME; + +/// The appearance of a tooltip. +#[must_use] +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// The amount of offset to apply to the shadow of the tooltip. + pub shadow_offset: Vector, + + /// The [`Background`] of the tooltip. + pub background: Option, + + /// The border radius of the tooltip. + pub border_radius: Radius, + + /// The border width of the tooltip. + pub border_width: f32, + + /// The border [`Color`] of the tooltip. + pub border_color: Color, + + /// An outline placed around the border. + pub outline_width: f32, + + /// The [`Color`] of the outline. + pub outline_color: Color, + + /// The icon [`Color`] of the tooltip. + pub icon_color: Option, + + /// The text [`Color`] of the tooltip. + pub text_color: Color, +} + +impl Style { + // TODO: `Radius` is not `const fn` compatible. + pub fn new() -> Self { + let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; + Self { + shadow_offset: Vector::new(0.0, 0.0), + background: None, + border_radius: Radius::from(rad_0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + outline_width: 0.0, + outline_color: Color::TRANSPARENT, + icon_color: None, + text_color: Color::BLACK, + } + } +} + +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 tooltip. +pub trait Catalog { + /// The supported style of the [`StyleSheet`]. + type Class: Default; + + /// Produces the active [`Appearance`] of a tooltip. + fn style(&self, style: &Self::Class) -> Style; +} diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs new file mode 100644 index 00000000..033ad0f3 --- /dev/null +++ b/src/widget/wayland/tooltip/widget.rs @@ -0,0 +1,684 @@ +// Copyright 2019 H�ctor Ram�n, Iced contributors +// Copyright 2023 System76 +// SPDX-License-Identifier: MIT + +//! Allow your users to perform actions by pressing a button. +//! +//! A [`Tooltip`] has some local [`State`]. + +use std::any::Any; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use iced::Task; +use iced_runtime::core::widget::Id; + +use iced_core::event::{self, Event}; +use iced_core::renderer; +use iced_core::touch; +use iced_core::widget::tree::{self, Tree}; +use iced_core::widget::Operation; +use iced_core::{layout, svg}; +use iced_core::{mouse, Border}; +use iced_core::{overlay, Shadow}; +use iced_core::{ + Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, +}; + +pub use super::{Catalog, Style}; + +/// Internally defines different button widget variants. +enum Variant { + Normal, + Image { + close_icon: svg::Handle, + on_remove: Option, + }, +} + +/// A generic button which emits a message when pressed. +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Tooltip<'a, Message, TopLevelMessage> { + id: Id, + #[cfg(feature = "a11y")] + name: Option>, + #[cfg(feature = "a11y")] + description: Option>, + #[cfg(feature = "a11y")] + label: Option>, + content: crate::Element<'a, Message>, + on_leave: Message, + on_surface_action: Box Message>, + width: Length, + height: Length, + padding: Padding, + selected: bool, + style: crate::theme::Tooltip, + delay: Option, + settings: Option< + Arc< + dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + >, + >, + view: Arc< + dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +} + +impl<'a, Message, TopLevelMessage> Tooltip<'a, Message, TopLevelMessage> { + /// Creates a new [`Tooltip`] with the given content. + pub fn new( + content: impl Into>, + settings: Option< + impl Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + >, + view: impl Fn() -> crate::Element<'static, crate::Action> + + Send + + Sync + + 'static, + on_leave: Message, + on_surface_action: impl Fn(crate::surface::Action) -> Message + 'static, + ) -> Self { + Self { + id: Id::unique(), + #[cfg(feature = "a11y")] + name: None, + #[cfg(feature = "a11y")] + description: None, + #[cfg(feature = "a11y")] + label: None, + content: content.into(), + width: Length::Shrink, + height: Length::Shrink, + padding: Padding::new(0.0), + selected: false, + style: crate::theme::Tooltip::default(), + on_leave, + on_surface_action: Box::new(on_surface_action), + delay: None, + settings: if let Some(s) = settings { + Some(Arc::new(s)) + } else { + None + }, + view: Arc::new(view), + } + } + + pub fn delay(mut self, dur: Duration) -> Self { + self.delay = Some(dur); + self + } + + /// Sets the [`Id`] of the [`Tooltip`]. + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + /// Sets the width of the [`Tooltip`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Tooltip`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the [`Padding`] of the [`Tooltip`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the widget to a selected state. + /// + /// Displays a selection indicator on image buttons. + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + + self + } + + /// Sets the style variant of this [`Tooltip`]. + pub fn class(mut self, style: crate::theme::Tooltip) -> Self { + self.style = style; + self + } + + #[cfg(feature = "a11y")] + /// Sets the name of the [`Tooltip`]. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Tooltip`]. + pub fn description_widget(mut self, description: &T) -> Self { + self.description = Some(iced_accessibility::Description::Id( + description.description(), + )); + self + } + + #[cfg(feature = "a11y")] + /// Sets the description of the [`Tooltip`]. + pub fn description(mut self, description: impl Into>) -> Self { + self.description = Some(iced_accessibility::Description::Text(description.into())); + self + } + + #[cfg(feature = "a11y")] + /// Sets the label of the [`Tooltip`]. + pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self { + self.label = Some(label.label().into_iter().map(|l| l.into()).collect()); + self + } +} + +impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> + Widget for Tooltip<'a, Message, TopLevelMessage> +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.content)); + } + + fn size(&self) -> iced_core::Size { + iced_core::Size::new(self.width, self.height) + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + }, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn Operation<()>, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.content.as_widget().operate( + &mut tree.children[0], + layout.children().next().unwrap(), + renderer, + operation, + ); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let status = update( + self.id.clone(), + event.clone(), + layout, + cursor, + shell, + self.settings.as_ref(), + &self.view, + self.delay, + &self.on_leave, + &self.on_surface_action, + || tree.state.downcast_mut::(), + ); + status.merge(self.content.as_widget_mut().on_event( + &mut tree.children[0], + event, + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + )) + } + + #[allow(clippy::too_many_lines)] + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + if !viewport.intersects(&bounds) { + return; + } + let content_layout = layout.children().next().unwrap(); + + let state = tree.state.downcast_ref::(); + + let styling = theme.style(&self.style); + + let icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); + + draw::<_, crate::Theme>( + renderer, + bounds, + *viewport, + &styling, + |renderer, _styling| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + &renderer::Style { + icon_color, + text_color: styling.text_color, + scale_factor: renderer_style.scale_factor, + }, + content_layout, + cursor, + &viewport.intersection(&bounds).unwrap_or_default(), + ); + }, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &crate::Renderer, + mut translation: Vector, + ) -> Option> { + let 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, + ) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + p: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let c_layout = layout.children().next().unwrap(); + + self.content.as_widget().a11y_nodes(c_layout, state, p) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static> + From> for crate::Element<'a, Message> +{ + fn from(button: Tooltip<'a, Message, TopLevelMessage>) -> Self { + Self::new(button) + } +} + +/// The local state of a [`Tooltip`]. +#[derive(Debug, Clone, Default)] +#[allow(clippy::struct_field_names)] +pub struct State { + is_hovered: Arc>, +} + +impl State { + /// Returns whether the [`Tooltip`] is currently hovered or not. + pub fn is_hovered(self) -> bool { + let guard = self.is_hovered.lock().unwrap(); + *guard + } +} + +/// Processes the given [`Event`] and updates the [`State`] of a [`Tooltip`] +/// accordingly. +#[allow(clippy::needless_pass_by_value)] +pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( + _id: Id, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, + settings: Option< + &Arc< + dyn Fn(Rectangle) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + >, + >, + view: &Arc< + dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, + delay: Option, + on_leave: &Message, + on_surface_action: &dyn Fn(crate::surface::Action) -> Message, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Touch(touch::Event::FingerLifted { .. }) => { + let state = state(); + let mut guard = state.is_hovered.lock().unwrap(); + if *guard { + *guard = false; + + shell.publish(on_leave.clone()); + + return event::Status::Captured; + } + } + + Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { + let state = state(); + let mut guard = state.is_hovered.lock().unwrap(); + + if *guard { + *guard = false; + + shell.publish(on_leave.clone()); + } + } + + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let state = state(); + let bounds = layout.bounds(); + let is_hovered = state.is_hovered.clone(); + let mut guard = state.is_hovered.lock().unwrap(); + + if *guard { + *guard = cursor.is_over(bounds); + if !*guard { + shell.publish(on_leave.clone()); + } + } else { + *guard = cursor.is_over(bounds); + if *guard { + if let Some(settings) = settings { + if let Some(delay) = delay { + let s = settings.clone(); + let view = view.clone(); + let bounds = layout.bounds(); + + let sm = crate::surface::Action::Task(Arc::new(move || { + let s = s.clone(); + let view = view.clone(); + let is_hovered = is_hovered.clone(); + Task::future(async move { + #[cfg(feature = "tokio")] + { + _ = tokio::time::sleep(delay).await; + } + #[cfg(feature = "async-std")] + { + _ = async_std::task::sleep(delay).await; + } + let is_hovered = is_hovered.clone(); + let g = is_hovered.lock().unwrap(); + if !*g { + return crate::surface::Action::Ignore; + } + let boxed: Box< + dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(move || s(bounds)); + let boxed: Box = + Box::new(boxed); + crate::surface::Action::Popup( + Arc::new(boxed), + Some({ + let boxed: Box< + dyn Fn() -> crate::Element< + 'static, + crate::Action, + > + Send + + Sync + + 'static, + > = Box::new(move || view()); + let boxed: Box = + Box::new(boxed); + Arc::new(boxed) + }), + ) + }) + })); + + shell.publish((on_surface_action)(sm)); + } else { + let s = settings.clone(); + let view = view.clone(); + let bounds = layout.bounds(); + + let boxed: Box< + dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(move || s(bounds)); + let boxed: Box = Box::new(boxed); + + let sm = crate::surface::Action::Popup( + Arc::new(boxed), + Some({ + let boxed: Box< + dyn Fn() -> crate::Element< + 'static, + crate::Action, + > + Send + + Sync + + 'static, + > = Box::new(move || view()); + let boxed: Box = + Box::new(boxed); + Arc::new(boxed) + }), + ); + shell.publish((on_surface_action)(sm)); + } + } + } + } + } + _ => {} + } + + event::Status::Ignored +} + +#[allow(clippy::too_many_arguments)] +pub fn draw( + renderer: &mut Renderer, + bounds: Rectangle, + viewport_bounds: Rectangle, + styling: &super::Style, + draw_contents: impl FnOnce(&mut Renderer, &Style), +) where + Theme: super::Catalog, +{ + let doubled_border_width = styling.border_width * 2.0; + let doubled_outline_width = styling.outline_width * 2.0; + + if styling.outline_width > 0.0 { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x - styling.border_width - styling.outline_width, + y: bounds.y - styling.border_width - styling.outline_width, + width: bounds.width + doubled_border_width + doubled_outline_width, + height: bounds.height + doubled_border_width + doubled_outline_width, + }, + border: Border { + width: styling.outline_width, + color: styling.outline_color, + radius: styling.border_radius, + }, + shadow: Shadow::default(), + }, + Color::TRANSPARENT, + ); + } + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + width: bounds.width, + height: bounds.height, + }, + border: Border { + radius: styling.border_radius, + ..Default::default() + }, + shadow: Shadow::default(), + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + // Draw the button background first. + if let Some(background) = styling.background { + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + radius: styling.border_radius, + ..Default::default() + }, + shadow: Shadow::default(), + }, + background, + ); + } + + // Then draw the button contents onto the background. + draw_contents(renderer, styling); + + let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default(); + clipped_bounds.height += styling.border_width; + + renderer.with_layer(clipped_bounds, |renderer| { + // Finish by drawing the border above the contents. + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + width: styling.border_width, + color: styling.border_color, + radius: styling.border_radius, + }, + shadow: Shadow::default(), + }, + Color::TRANSPARENT, + ); + }); + } else { + draw_contents(renderer, styling); + } +} + +/// Computes the layout of a [`Tooltip`]. +pub fn layout( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); + + let mut content = layout_content(renderer, &limits.shrink(padding)); + let padding = padding.fit(content.size(), limits.max()); + let size = limits + .shrink(padding) + .resolve(width, height, content.size()) + .expand(padding); + + content = content.move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(size, vec![content]) +} diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs new file mode 100644 index 00000000..219254c7 --- /dev/null +++ b/src/widget/wrapper.rs @@ -0,0 +1,220 @@ +use std::{ + cell::RefCell, + rc::Rc, + thread::{self, ThreadId}, +}; + +use crate::Element; +use iced::{event, Length, Rectangle, Size}; +use iced_core::{id::Id, widget, widget::tree, Widget}; + +#[derive(Debug)] +pub struct RcWrapper { + pub(crate) data: Rc>, + pub(crate) thread_id: ThreadId, +} + +impl Clone for RcWrapper { + fn clone(&self) -> Self { + Self { + data: self.data.clone(), + thread_id: self.thread_id, + } + } +} + +unsafe impl Send for RcWrapper {} +unsafe impl Sync for RcWrapper {} + +impl RcWrapper { + pub fn new(element: T) -> Self { + Self { + data: Rc::new(RefCell::new(element)), + thread_id: thread::current().id(), + } + } + + /// # Panics + /// + /// Will panic if used outside of original thread. + pub fn with_data(&self, f: impl FnOnce(&T) -> O) -> O { + assert_eq!(self.thread_id, thread::current().id()); + let my_ref: &T = &RefCell::borrow(self.data.as_ref()); + f(my_ref) + } + + /// # Panics + /// + /// Will panic if used outside of original thread. + pub fn with_data_mut(&self, f: impl FnOnce(&mut T) -> O) -> O { + assert_eq!(self.thread_id, thread::current().id()); + let my_refmut: &mut T = &mut RefCell::borrow_mut(self.data.as_ref()); + f(my_refmut) + } + + /// # Panics + /// + /// Will panic if used outside of original thread. + pub(crate) unsafe fn as_ptr(&self) -> *mut T { + assert_eq!(self.thread_id, thread::current().id()); + RefCell::as_ptr(self.data.as_ref()) + } +} + +#[derive(Clone)] +pub struct RcElementWrapper { + pub(crate) element: RcWrapper>, +} + +impl RcElementWrapper { + #[must_use] + pub fn new(element: Element<'static, M>) -> Self { + RcElementWrapper { + element: RcWrapper::new(element), + } + } +} + +impl Widget for RcElementWrapper { + fn size(&self) -> Size { + self.element.with_data(|e| e.as_widget().size()) + } + + fn size_hint(&self) -> Size { + self.element.with_data(move |e| e.as_widget().size_hint()) + } + + fn layout( + &self, + tree: &mut tree::Tree, + renderer: &crate::Renderer, + limits: &crate::iced_core::layout::Limits, + ) -> crate::iced_core::layout::Node { + self.element + .with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits)) + } + + fn draw( + &self, + tree: &tree::Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &crate::iced_core::renderer::Style, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, + viewport: &Rectangle, + ) { + self.element.with_data(move |e| { + e.as_widget() + .draw(tree, renderer, theme, style, layout, cursor, viewport); + }); + } + + fn tag(&self) -> tree::Tag { + self.element.with_data(|e| e.as_widget().tag()) + } + + fn state(&self) -> tree::State { + self.element.with_data(|e| e.as_widget().state()) + } + + fn children(&self) -> Vec { + self.element.with_data(|e| e.as_widget().children()) + } + + fn diff(&mut self, tree: &mut tree::Tree) { + self.element.with_data_mut(|e| e.as_widget_mut().diff(tree)); + } + + fn operate( + &self, + state: &mut tree::Tree, + layout: crate::iced_core::Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn widget::Operation, + ) { + self.element.with_data(|e| { + e.as_widget().operate(state, layout, renderer, operation); + }); + } + + fn on_event( + &mut self, + state: &mut tree::Tree, + event: crate::iced::Event, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn crate::iced_core::Clipboard, + shell: &mut crate::iced_core::Shell<'_, M>, + viewport: &Rectangle, + ) -> event::Status { + self.element.with_data_mut(|e| { + e.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, viewport, + ) + }) + } + + fn mouse_interaction( + &self, + state: &tree::Tree, + layout: crate::iced_core::Layout<'_>, + cursor: crate::iced_core::mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> crate::iced_core::mouse::Interaction { + self.element.with_data(|e| { + e.as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + }) + } + + fn overlay<'a>( + &'a mut self, + state: &'a mut tree::Tree, + layout: crate::iced_core::Layout<'_>, + renderer: &crate::Renderer, + translation: crate::iced_core::Vector, + ) -> Option> { + assert_eq!(self.element.thread_id, thread::current().id()); + Rc::get_mut(&mut self.element.data).and_then(|e| { + e.get_mut() + .as_widget_mut() + .overlay(state, layout, renderer, translation) + }) + } + + fn id(&self) -> Option { + self.element.with_data_mut(|e| e.as_widget_mut().id()) + } + + fn set_id(&mut self, id: Id) { + self.element.with_data_mut(|e| e.as_widget_mut().set_id(id)); + } + + fn drag_destinations( + &self, + state: &tree::Tree, + layout: crate::iced_core::Layout<'_>, + renderer: &crate::Renderer, + dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles, + ) { + self.element.with_data_mut(|e| { + e.as_widget_mut() + .drag_destinations(state, layout, renderer, dnd_rectangles); + }); + } +} + +impl From> for Element<'static, Message> { + fn from(wrapper: RcElementWrapper) -> Self { + Element::new(wrapper) + } +} + +impl From> for RcElementWrapper { + fn from(e: Element<'static, Message>) -> Self { + RcElementWrapper::new(e) + } +}